diff --git a/app/__init__.py b/app/__init__.py index b6cefa6..2ec71df 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -8,6 +8,8 @@ from typing import Optional from flask import Flask, Response, abort, send_from_directory, render_template from markdown import markdown +from .latex import MarkdownLatex + basedir = os.path.abspath(os.path.join(os.path.realpath(__file__), '..', '..')) templates_dir = os.path.join(basedir, 'templates') static_dir = os.path.join(basedir, 'static') @@ -66,7 +68,7 @@ def get_page(page: str, title: Optional[str] = None, skip_header: bool = False): author_email=re.match(r'(.+?)\s+<([^>]+)>', metadata['author'])[2] if 'author' in metadata else None, published=(metadata['published'].strftime('%b %d, %Y') if metadata.get('published') else None), - content=markdown(f.read(), extensions=['fenced_code', 'codehilite']), + content=markdown(f.read(), extensions=['fenced_code', 'codehilite', MarkdownLatex()]), skip_header=skip_header) diff --git a/app/latex.py b/app/latex.py new file mode 100644 index 0000000..290f67a --- /dev/null +++ b/app/latex.py @@ -0,0 +1,243 @@ +""" +Licensed under Public Domain Mark 1.0. +See https://creativecommons.org/publicdomain/mark/1.0/ +Author: Justin Bruce Van Horne + +Python-Markdown LaTeX Extension +Adds support for $math mode$ and %text mode%. This plugin supports +multiline equations/text. +The actual image generation is done via LaTeX/DVI output. +It encodes data as base64 so there is no need for images directly. +All the work is done in the preprocessor. +""" + +import base64 +import hashlib +import json +import os +import re +import tempfile +from subprocess import call as rawcall, PIPE + +import markdown + + +def call(*args, **kwargs): + """ + Proxy to subprocess.call(), removes timeout argument in case of + Python2 because that was only implemented in Python3. + """ + return rawcall(*args, **kwargs) + + +# Defines our basic inline image +img_expr = '%s' + +# Defines multiline expression image +multiline_img_expr = '''
+%s
''' + +# Base CSS template +img_css = """""" + +# Cache and temp file paths +tmpdir = tempfile.gettempdir() + '/markdown-latex' +cache_file = tmpdir + '/latex.cache' + + +class LaTeXPreprocessor(markdown.preprocessors.Preprocessor): + # These are our cached expressions that are stored in latex.cache + cached = {} + + # Basic LaTex Setup as well as our list of expressions to parse + tex_preamble = r"""\documentclass[14pt]{article} +\usepackage{amsmath} +\usepackage{amsthm} +\usepackage{amssymb} +\usepackage{bm} +\usepackage{graphicx} +\usepackage[usenames,dvipsnames]{color} +\pagestyle{empty} +""" + + # Math TeX extraction regex + math_extract_regex = re.compile(r'(.+?)((\\\(.+?\\\))|(\$\$\n.+?\n\$\$\n))(.*)', re.MULTILINE | re.DOTALL) + + # Math TeX matching regex + math_match_regex = re.compile(r'\s*(\\\(.+?\\\))|(\$\$\n.+?\n\$\$\n)\s*', re.MULTILINE | re.DOTALL) + + def __init__(self, *_, **__): + if not os.path.isdir(tmpdir): + os.makedirs(tmpdir) + try: + with open(cache_file, 'r') as f: + self.cached = json.load(f) + except (IOError, json.JSONDecodeError): + self.cached = {} + + self.config = { + ("general", "preamble"): "", + ("dvipng", "args"): "-q -T tight -bg Transparent -z 9 -D 200", + ("delimiters", "text"): "%", + ("delimiters", "math"): "$", + ("delimiters", "preamble"): "%%"} + + def _latex_to_base64(self, tex): + """Generates a base64 representation of TeX string""" + + # Generate the temporary file + tmp_file_fd, path = tempfile.mkstemp(dir=tmpdir) + with os.fdopen(tmp_file_fd, "w") as tmp_file: + tmp_file.write(self.tex_preamble) + tmp_file.write(tex) + tmp_file.write('\n\\end{document}') + + # compile LaTeX document. A DVI file is created + status = call(('latex -halt-on-error -output-directory={:s} {:s}' + .format(tmpdir, path)).split(), + stdout=PIPE, timeout=10) + + # clean up if the above failed + if status: + self._cleanup(path, err=True) + raise Exception("Couldn't compile LaTeX document." + + "Please read '%s.log' for more detail." % path) + + # Run dvipng on the generated DVI file. Use tight bounding box. + # Magnification is set to 1200 + dvi = "%s.dvi" % path + png = "%s.png" % path + + # Extract the image + cmd = "dvipng %s %s -o %s" % (self.config[("dvipng", "args")], dvi, png) + status = call(cmd.split(), stdout=PIPE) + + # clean up if we couldn't make the above work + if status: + self._cleanup(path, err=True) + raise Exception("Couldn't convert LaTeX to image." + + "Please read '%s.log' for more detail." % path) + + # Read the png and encode the data + try: + with open(png, "rb") as png: + data = png.read() + return base64.b64encode(data) + finally: + self._cleanup(path) + + @staticmethod + def _cleanup(path, err=False): + # don't clean up the log if there's an error + extensions = ["", ".aux", ".dvi", ".png", ".log"] + if err: + extensions.pop() + + # now do the actual cleanup, passing on non-existent files + for extension in extensions: + try: + os.remove("%s%s" % (path, extension)) + except (IOError, OSError): + pass + + def run(self, lines): + """Parses the actual page""" + # Re-creates the entire page so we can parse in a multine env. + page = "\n".join(lines) + + # Adds a preamble mode + self.tex_preamble += self.config[("general", "preamble")] + "\n\\begin{document}\n" + + # Figure out our text strings and math-mode strings + tex_expr = self.math_extract_regex.findall(page) + + # No sense in doing the extra work + if not len(tex_expr): + return page.split("\n") + + # Parse the expressions + new_cache = {} + new_page = '' + n_multiline_expressions = 0 + + while page: + m = self.math_extract_regex.match(page) + if not m: + new_page += page + break + + new_page += m.group(1) + math_match = self.math_match_regex.match(m.group(2)) + if not math_match: + new_page += m.group(2) + else: + expr = m.group(2) + is_multiline = math_match.group(2) is not None + tex_hash = self.hash(expr) + if tex_hash in self.cached: + data = self.cached[tex_hash] + else: + data = self._latex_to_base64(expr).decode() + new_cache[tex_hash] = data + + if is_multiline and n_multiline_expressions > 0: + new_page += '

' + new_page += (multiline_img_expr if is_multiline else img_expr) % ('true', expr, tex_hash, data) + + if is_multiline: + new_page += '

' + n_multiline_expressions += 1 + + page = m.group(5) + + if n_multiline_expressions > 0: + new_page += '

' + + # Cache our data + self.cached.update(new_cache) + with open(cache_file, 'w') as f: + json.dump(self.cached, f) + + # Make sure to re-split the lines + return new_page.split("\n") + + @staticmethod + def hash(tex: str) -> str: + return hashlib.sha1(tex.encode()).hexdigest() + + +class LaTeXPostprocessor(markdown.postprocessors.Postprocessor): + """This post processor extension just allows us to further + refine, if necessary, the document after it has been parsed.""" + + # noinspection PyMethodMayBeStatic + def run(self, text): + # Inline a style for default behavior + text = img_css + text + return text + + +class MarkdownLatex(markdown.Extension): + """Wrapper for LaTeXPreprocessor""" + + def extendMarkdown(self, md): + # Our base LaTeX extension + md.preprocessors.add('latex', + LaTeXPreprocessor(self), ">html_block") + # Our cleanup postprocessing extension + md.postprocessors.add('latex', + LaTeXPostprocessor(self), ">amp_substitute") diff --git a/static/img/adafruit-16-pwm.jpg b/static/img/adafruit-16-pwm.jpg new file mode 100644 index 0000000..5cf8ce9 Binary files /dev/null and b/static/img/adafruit-16-pwm.jpg differ diff --git a/static/img/brushless-motor-1.png b/static/img/brushless-motor-1.png new file mode 100644 index 0000000..e59c152 Binary files /dev/null and b/static/img/brushless-motor-1.png differ diff --git a/static/img/drone-lift.gif b/static/img/drone-lift.gif new file mode 100644 index 0000000..9fe8a0d Binary files /dev/null and b/static/img/drone-lift.gif differ diff --git a/static/img/drone-schema.png b/static/img/drone-schema.png new file mode 100644 index 0000000..9842ba0 Binary files /dev/null and b/static/img/drone-schema.png differ diff --git a/static/img/drone-warning.png b/static/img/drone-warning.png new file mode 100644 index 0000000..afafb2f Binary files /dev/null and b/static/img/drone-warning.png differ diff --git a/static/img/esc-1.jpg b/static/img/esc-1.jpg new file mode 100644 index 0000000..5ce61aa Binary files /dev/null and b/static/img/esc-1.jpg differ diff --git a/static/img/esc-pwm.png b/static/img/esc-pwm.png new file mode 100644 index 0000000..d01d7a9 Binary files /dev/null and b/static/img/esc-pwm.png differ diff --git a/static/img/lipo-1.jpg b/static/img/lipo-1.jpg new file mode 100644 index 0000000..98d995c Binary files /dev/null and b/static/img/lipo-1.jpg differ diff --git a/static/img/pitch-roll-yaw-1.png b/static/img/pitch-roll-yaw-1.png new file mode 100644 index 0000000..a54bcdc Binary files /dev/null and b/static/img/pitch-roll-yaw-1.png differ diff --git a/static/img/pitch-roll-yaw-2.png b/static/img/pitch-roll-yaw-2.png new file mode 100644 index 0000000..f5c712a Binary files /dev/null and b/static/img/pitch-roll-yaw-2.png differ diff --git a/static/img/propeller-1.png b/static/img/propeller-1.png new file mode 100644 index 0000000..945d220 Binary files /dev/null and b/static/img/propeller-1.png differ diff --git a/static/img/propeller-schema.png b/static/img/propeller-schema.png new file mode 100644 index 0000000..8554550 Binary files /dev/null and b/static/img/propeller-schema.png differ diff --git a/static/img/pwm-1.png b/static/img/pwm-1.png new file mode 100644 index 0000000..d58b9d2 Binary files /dev/null and b/static/img/pwm-1.png differ diff --git a/static/img/rpi-pinout.jpg b/static/img/rpi-pinout.jpg new file mode 100644 index 0000000..dbff511 Binary files /dev/null and b/static/img/rpi-pinout.jpg differ diff --git a/static/img/static-thrust-1.png b/static/img/static-thrust-1.png new file mode 100644 index 0000000..7bccf21 Binary files /dev/null and b/static/img/static-thrust-1.png differ diff --git a/static/img/xt60-board.jpg b/static/img/xt60-board.jpg new file mode 100644 index 0000000..bfb92d8 Binary files /dev/null and b/static/img/xt60-board.jpg differ diff --git a/static/pages/Build-an-open-source-drone-with-a-Raspberry-Pi-and-Platypush.md b/static/pages/Build-an-open-source-drone-with-a-Raspberry-Pi-and-Platypush.md new file mode 100644 index 0000000..a0f5dbe --- /dev/null +++ b/static/pages/Build-an-open-source-drone-with-a-Raspberry-Pi-and-Platypush.md @@ -0,0 +1,1563 @@ +[//]: # (title: Design and build a drone from scratch) +[//]: # (description: How to use a Raspberry Pi, Platypush and some cheap electronics to build your own drone.) +[//]: # (image: /img/drone-schema.png) +[//]: # (author: Fabio Manganiello ) +[//]: # (published: 2021-08-23) + +Drones are increasingly popular and affordable nowadays, and they have become a popular toy for kids and adults +of all ages. If all you need is a flying camera to take your holiday selfies then an off-the-shelf solution may +be good enough. If instead you want something a bit more customized, or you just enjoy the pleasure of building +things from scratch to understand how they work (and few things give more satisfaction than seeing something you have +built lift off the ground), then you are in the right place. + +This article is about designing and building a drone completely from scratch and is divided in four parts. + +The first part covers the physics of lift, both under an aerodynamic (how to pick the right motors and blades and +how to use them to generate lift) and an electric perspective (how lift power and time-of-flight relate to battery +capacity and discharge rate and how to optimize your design). + +The second part covers the hardware side, how to get the frame in place, which electronic components to pick, how to +wire them together, how to place them on the body of the drone and how PWM modulation works. + +The third part covers the software side, specifically how to code a flight controller on a Raspberry Pi as set of +Python scripts that run on top of [Platypush](https://platypush.tech). + +The fourth part shows how to put all things together, calibrate and fly your new drone. + +## Disclaimer + +A disclaimer is owed before we start. + +Drones are devices that use rigid propellers that spin hundreds or thousands of times in a minute. That's at least ten +times faster than your regular table fan. Needless to say, if those things touch your fingers you can get very, very +hurt. Moreover, these propellers are usually powered by high-power lithium batteries that spit out lots of current to +ensure that they can run so fast. If any of the connections between your propellers and the battery are loose, you may +get some bad sparks as soon as you try and fly your drone - and nothing nice can happen if those sparks touch the +battery. + +So some precautions are due before you assemble and start anything: + +1. **NEVER**, ever run your drone with the blades mounted unless you have completed the calibration phase, and you + are **REALLY** sure that you can quickly switch everything off in case your drone leaps towards your face. + +2. As a corollary of the point above: **ALWAYS** make sure that you have a kill switch (either hardware or software) + to physically cut the power from the motors before your drone damages objects or living beings. + +3. Second corollary: when you are in the initial calibration phase of your drone, or you are checking that all the + connections are fine and that the motors spin in the right direction, **ALWAYS** do it without the blades mounted. + +4. **ALWAYS** make sure that the drone has enough room around before starting the motors, and that no wires nor other + mechanical obstacles are on the path of the propellers when they spin. + +5. **ALWAYS** pay attention when you work with large LiPo batteries. They are amazing and can store a lot of juice, but + they are also ready to go on fire on the first mistake. Make sure that none of the moving parts is ever touching the + battery - even a small puncture can cause the battery to smoke, as the oxygen from the air comes in contact with the + electrolytes of the battery. **NEVER** overcharge them and never over-discharge them - refer to the manual for the + advised voltage range. **NEVER** short-circuit them. + +6. **ALWAYS** make sure that all the connections between the battery and the motors (including those to the power board, + to the ESCs and to the motors) are stable and well soldered. When you install electronics on things that can move so + fast, even a small loose connection can be a recipe for disaster. Also, make sure that all the wires and electrolyte + capacitors are installed with the right polarity. + +7. Consider installing physical protections for your propellers. + +8. If you are planning to fly the drone outdoor, remember to check your local rules, and always behave responsibly: + after all, you are flying DIY vehicles in public areas. + +![Drone warning](../img/drone-warning.png) + +With these recommendations out of the way, let's start getting our hands dirty with a bit of theory about drones and +lift. + +## The physics of a drone + +### The physics of lift + +This section will include some theory on how to get drones (and things in general) to lift and fly. I would advise to +do these calculations *before* you start assembling or even order components for your drone: flying objects rely on +delicate physical balances. If you get any of the variables wrong (e.g. you assemble a drone that is too heavy for what +the propellers can lift, or you install a battery that can't provide enough current for lift-off) then you'll only +notice it when the drone is completely assembled, and it'll take time, money and effort to disassemble the drone, +redesign it and reorder components in order to address these issues (I have learned this the hard way). Time to dust +off some physics books! + +A quadcopter flies by displacing air through the quick rotations of its propellers. The blades of a propeller share a +similar structure with wings, and both are designed to move air at a higher speed above them than the air below. A wing +is designed to "cut" through the air at high speeds, causing the air above it to move as fast as the wing but in the +opposite direction (3rd Newton's law a.k.a. _action-and-reaction_). This movement ends up creating a pocket of air with +lower pressure and higher speed on the top of the blade. This volume of air is forced to flow downwards along the +structure, and it meets the bottom flow of air at the tip of the blade. This movement causes the air at the bottom of +the blade to apply an upward pressure towards the bottom of the propeller that is higher than the downward pressure +applied to the top. The imbalance between the pressure of the two columns of air (above and below the blade) is what +causes the attached structure to lift. + +![Drone lift diagram](../img/drone-lift.gif) + +The first fundamental definition when it comes to drone flight is that of **static thrust** \(T_0\). The static thrust +is informally defined as the force that the propellers need to provide to the vehicle to maintain it in _stationary_ +conditions. When the drone is _hovering_, this is basically the force that the propellers need to apply to the vehicle +in order to balance its weight and keep it in position. Calculating this force is fundamental to design any vehicle that +can fly, albeit it's not a sufficient condition: to actually get a vehicle to lift off you first need to apply a force +\(T > T_0\), so you should usually design a drone in such a way that its propellers can provide a thrust at least 25% +higher than its static thrust. + +![Static thrust force diagram](../img/static-thrust-1.png) + +Intuitively, the thrust provided by a propeller must be somehow proportional to the velocity of the air it displaces. +The higher the speed of the air it moves, the lower its pressure, the higher the gradient of pressure between the air +above and below, the higher the lift it can provide. A formalization of this intuition is provided by the +[Bernoulli's principle](https://en.wikipedia.org/wiki/Bernoulli%27s_principle), which states that air pressure _p_ +and velocity _v_ at any point in space are connected by the following relation: + +$$ +\frac{v^2}{2} + \frac{p}{\rho} = const +$$ + +where \(\rho\) is the density of the medium (in this case air). + +If we now define \(p_0\) as the pressure of air below the blade, \(p_1\) the pressure above, \(v_0\) as the speed +of air before it hits the blade and \(v_1 = v_0 + \Delta v\) as the speed of air after it is accelerated by the blade, +then we must have: + +$$ +p_0 + \frac{1}{2} \rho v_0^2 = p_1 + \frac{1}{2} \rho v_1^2 +$$ + +The difference in air pressure that generates the lift can therefore be written as: + +$$ +\Delta p = p_1 - p_0 = \frac{1}{2} \rho (v_1^2 - v_0^2) +$$ + +We can picture how air speed and pressure change when they are accelerated by a propeller with diameter _d_ (and +how the width of the column of air changes as well) through this diagram: + +![Diagram of the air flowing through a propeller](../img/propeller-1.png) + +We also know that pressure equals force divided by area, therefore the thrust must equal the difference of pressure +times the area of the propeller _A_: + +$$ +T = A \cdot \Delta p = \frac{1}{2} \rho A (v_1^2 - v_0^2) +$$ + +The area of a propeller moving at very high speed can be approximated as the area of a circular disc whose diameter +equals the diameter of the propellers: + +$$ +A = \pi \Big( \frac{d}{2} \Big)^2 = \frac{\pi}{4} d^2 +$$ + +Therefore we can rewrite the thrust as: + +$$ +T = \frac{\pi}{8} \rho d^2 (v_1^2 - v_0^2) +$$ + +If we replace \(v_1\) with \(v_0 + \Delta v\) we get: + +$$ +T = \frac{\pi}{4} \rho d^2 \Big(v_0 + \frac{\Delta v}{2}\Big) \Delta v +$$ + +We can assume that \(v_0 = 0\), i.e. the velocity of air far from the propellers is zero (this may not be a good +approximation when you operate the drone in a windy or highly turbulent environment though). With this assumption, the +formula can be simplified to: + +$$ +T = \frac{\pi}{8} \rho d^2 \Delta v^2 +$$ + +This equation provides two very valuable pieces of information when it comes to designing our drone: + +1. *The thrust is proportional to the square of the diameter of the propellers*. If you double the diameter of the + propellers then the thrust increases by 4. If you triple it then it increases by 9, and so on. However, larger + propellers tend to have larger mass, the motors have to apply a greater force to spin them, and they generate greater + air turbulence, resulting in drones that are harder to control. + +2. *The thrust is proportional to the square of the variation of speed of the air around the propellers*. Propellers + that can displace more air can generate more thrust. The more obvious way to increase the speed of air is to increase + the speed of the propellers by applying a higher current to the motors. Another way is to increase the number of + blades on the propellers (more blades will cause more air to move, which in turn causes a greater difference in + velocity), but a higher number of blades tends to also increase air turbulence. Usually a two blade configuration is + used for most of the commercial drones, while a three blade configuration is often used on racing drones or + high-power vehicles. Also keep in mind that the efficiency of a real propeller goes down when \(\Delta v\) becomes + too high (that's because a higher gradient of velocity means a higher turbulence when the air flowing on top of the + wing meets the air below at the edge of the blade), so you may want to strike a balance between thrust generated + by air displacement and propeller efficiency. + +Albeit useful, this equation isn't really the most used when it comes to quadcopter design. That's because it's tricky +to build a model that takes into account the velocity of air displaced by each single propeller. Instead, most of the +drones rely on electric batteries, and we have better information about battery power, drawn current and voltage than we +have about volumes of displaced air or gradients of pressure. So it's convenient to transform velocity into power _P_, +remembering that: + +$$ +P = \frac{\Delta E}{\Delta t} = \frac{F \cdot \Delta s}{\Delta t} = F \cdot v +$$ + +A common convention is to set _v_ as the median between the velocity of air before (\(v_0 = 0\)) and after +(\(v_1 = v_0 + \Delta v\)) hitting the blade: + +$$ +v = \frac{v_0 + v_1}{2} = \frac{\Delta v}{2} +$$ + +We can then rewrite the equation of power with respect to propeller thrust as: + +$$ +P = \frac{1}{2} T \Delta v +$$ + +In a real-case scenario, however, not all the energy drawn from the battery is converted into kinetic energy that spins +the propellers. Motors have their own electric efficiency \(\eta_{el}\), which depends on the losses caused by electric +energy transformed to heat during the rotation, and propellers also have their own mechanical efficiency \(\eta_ +{prop}\), mostly caused by the energy dissipated by the drag of the rotors against the air. A good brushless motor has +an electric efficiency around 90%, while a good propeller has a mechanical efficiency between 80-85%. Therefore, the +equation of the actual power that goes into lifting the drone should be rewritten as: + +$$ +P_{real} = \eta_{el} \cdot \eta_{prop} \cdot P = \frac{1}{2} \eta_{el} \eta_{prop} T \Delta v +$$ + +Solving for \(\Delta v\): + +$$ +\Delta v = \frac{2 \eta_{el} \eta_{prop} P}{T} +$$ + +Now that we have found a way to express the variation of air velocity in function of the power provided to the +propellers, we can rewrite the equation of thrust calculated previously as: + +$$ +T = \frac{\pi}{8} \rho d^2 \Delta v^2 \\ + = \sqrt[3]{\frac{\pi}{2} \rho d^2 \eta_{el}^2 \eta_{prop}^2 P^2} +$$ + +Finally, remembering that \(F = ma\), let's divide both the terms in the equation above by the gravity acceleration +_g_ to get a value for thrust expressed in kg: + +$$ +m = \frac{1}{g} \sqrt[3]{\frac{\pi}{2} \rho d^2 \eta_{el}^2 \eta_{prop}^2 P^2} +$$ + +This is a very useful equation that can be used to design our drone. If we set _m_ equal to the mass of the drone, +then we can solve for _P_ and calculate how much power we need to provide to the motors in order to generate a lift that +equals the mass of the drone - in other words, this is the power required to achieve static thrust. + +$$ +P = \frac{1}{d \eta_{el} \eta_{prop}} \sqrt{\frac{2 m^3 g^3}{\pi \rho}} +$$ + +We can easily wrap the above formula into a small Python function: + +```python +def balance_power(mass, propel_diam, motor_eff, propel_eff): + """ + Calculate how much power is required to provide a thrust to a + drone that matches its weight, given a certain configuration + of propellers and motors. + + :param mass: Mass (in kg) + :param propel_diam: Propellers diameter (in meters) + :param motor_eff: Motor efficiency (between 0 and 1) + :param propel_eff: Propellers efficiency (between 0 and 1) + :return: The power required to generate a lift that balances the weight + of the vehicle, in Watts + """ + import math + + # Air density at sea level and room temperature is about 1.225 kg/m^3 + density = 1.225 + # Gravity acceleration = 9.8 m/s^2 + g = 9.8 + + return ( + (1/(propel_diam * motor_eff * propel_eff)) * + math.sqrt((2 * math.pow(mass,3) * math.pow(g,3))/(math.pi * density)) + ) +``` + +For example, if you have a drone with the following characteristics: + +- _mass_: 500 grams +- _propellers diameter_: 5 inches (= 0.127 meters) +- _motors efficiency_: 90% +- _propellers diameter_: 80% + +We can infer that it takes about 85W of power to generate a lift that balances the weight: + +```python +>>> balance_power(mass=0.5, propel_diam=0.127, motor_eff=0.9, propel_eff=0.8) +85.51256229077079 +``` + +### The physics of electric vehicles + +The last formula brings us directly to the next topic: once we have figured the dynamics and the aerodynamics, and how +much work is required to lift our object, it's time to translate those constraints into electric constraints and size +battery, speed controllers and motors accordingly. Importantly, we should also design the system to be able to provide +more power than the power required to simply balance its weight - thrust needs to be greater than weight if we want the +drone to go up. + +You may probably need a powerful LiPo battery if you want to provide your motors with enough power to generate +lift for a drone that will sport a Raspberry Pi, a camera, a bunch of sensors, and who knows how much more stuff you are +planning to add :) high-power LiPo batteries usually come in packages like this: + +![Picture of a LiPo battery](../img/lipo-1.jpg) + +High-power LiPo batteries usually come with two types of connectors: XT60 (yellow adapter in the picture above) +or T-Plug (original dark red connector connected to the battery in the picture above). Always make sure that the +connector of your battery matches the connector of your power distribution board (adapters are available and cheap, but +they add a bit of weight and take extra space on the drone). + +A LiPo battery usually comes with an indication of its total capacity _Q_, usually expressed in mAh, and its +voltage, expressed in volts (keep in mind, however, that the advertised voltage goes down as the battery discharges, +and that also impacts the output power). + +You also have an indication of the **discharge rate**, usually indicated by a number suffixed by _C_. This number +is used to calculate the maximum current \(I_{max}\) that the battery can provide without resulting in damage to the +battery itself - this number is the most fundamental cap to the maximum power that a battery can provide. The maximum +current is calculated as the total capacity expressed in amperes divided by one hour and multiplied by the _C_ number. +For example, in the case of the battery pictured above, with an advertised _50C_ value and _5200 mAh_ capacity, we have: + +$$ +I_{max} = 5.2 \cdot 50 = 260 A +$$ + +Now that we have the tools to measure the capacity, voltage and maximum current supported by the battery, we can +substitute \(P = IV\) in the previous equation of power and, assuming that the voltage is about constant, we can +calculate how much current the motors will absorb to generate a certain lift: + +$$ +I = \frac{P}{V} = \frac{1}{Vd \eta_{el} \eta_{prop}} \sqrt{\frac{2 m^3 g^3}{\pi \rho}} +$$ + +For example, if the minimum power required to provide stationary thrust is 85W, and the battery works with a tension of +11.1V, then the average current that will be absorbed by the motors while hovering is about 7.6A. + +It is _very_ important that this current is lower than \(I_{max}\) - ideally it should be a fraction of \(I_{max}\), +or at least half of it to give some headroom during current peaks, especially during lift off, in order to prevent +permanent damage to the battery, so choose a battery with an appropriate discharge rate for the physical characteristics +of your vehicle. + +Finally, we can calculate how long the battery will be able to provide a certain current. Knowing that +\(I = \frac{\Delta Q}{\Delta t}\), we can express time as a function of battery capacity and required power as: + +$$ +\Delta t = \frac{Q}{I} = \frac{QV}{IV} = \frac{E}{P} +$$ + +Where _E_ is the total energy stored in the battery and _P_ is the power we want to provide to the load. + +Suppose that a 5000 mAh, 11.V battery is used on our previous 500 grams drone. Then in an ideal case the battery can +provide a power of 85W (8A * 11.1V) for: + +$$ +\Delta t = \frac{5A * 3600\mbox{ sec} * 11.1V}{85W} = 39\mbox{ min} +$$ + +In reality this value is usually lower (usually at least a half or a third of it, depending on the minimum current +required by the motors to spin) with a non-ideal battery, because below a certain charge left the battery won't be able +to provide the same nominal values of voltage and current, while the formula above assumes that the battery can provide +the same power until the last bit of charge left. However, you can empirically estimate the amount of charge left in the +battery when it starts to struggle to provide the motors with enough torque (you can hear the motors slowing down when +this happens), replace _E_ in the equation above with \(\Delta E = E_{max} - E_ {min}\), and you can get a more +realistic estimate of the maximum flight time. + +### Theory in practice + +The math that governs the physics of lift and electric power may take a while to sink in, but we can briefly sum up the +key takeaways from the formulas above when it comes to designing your drone: + +- The power you need to provide to lift your drone is inversely proportional to the diameter of the blades (larger + blades mean more air moved by the propellers, therefore more thrust, therefore less power required). However, larger + blades also add up to the total mass, and they also generate greater turbulence, making the vehicle harder to control. + +- The power you need to provide to lift your drone is proportional to the 3/2 power of its mass (a bit more than a + linear dependency, a bit less than a squared dependency). So beware of entering the vicious cycle where greater mass + requires bigger battery and bigger propellers, which in turn add mass, which in turn requires greater power, and so + on. + +- The discharge rate (_C_) of a battery is an important factor that you should take into account before selecting a + battery. Make sure that the battery can provide enough current to achieve static balance given the physical + characteristics of your drone. + +- The maximum time of flight for a drone (roughly calculated as the time that a battery can provide enough lift to + counteract its weight) in an ideal scenario equals the total energy stored in the battery divided by the power + required to achieve static balance. + +- In reality, this quantity assumes that the battery can provide the same power until the last drop of juice left, which + is usually not the case for real batteries. For a more accurate number, you should estimate how much charge is left in + your battery when the motors start to slow down, and calculate the total time in function of that \(\Delta E\) + difference. + +- Larger batteries can provide longer flight time, but they also add up to the total mass, so you may want to find an + appropriate trade-off for your case. + +Time to get our hands dirty with the real thing now! + +## The hardware of a drone + +### Components of a quadcopter + +- **Flight controller**: This is the "brain" of the drone. It receives commands from an input source and sends signals + down the line to control the motors. This is usually the most expensive component of a drone if you want to buy an + off-the-shelf circuit. We will be using a Raspberry Pi Zero as a flight controller in this article. That makes the + final cost of the drone considerably lower, but it also requires us to write the code for receiving remote commands + and for sending the correct signals downstream to the motors. + +- **Propellers**: These are technically the only moving parts of the vehicle. We have already covered previously how + their diameter affects the generated thrust, the required power and the level of air turbulence. Another important + consideration is about the number of blades. 2-blades propellers often result in vehicles that are easier to control, + even though they may not provide the same acceleration. 3-blades configuration are a bit harder to control, but they + usually provide greater acceleration and are preferred for larger drones or racing configurations. The material of + the propellers also plays a role, both in terms of robustness and added weight. Carbon fibers propellers are usually + preferred in professional applications because of their lower weight and greater robustness, but they come at a higher + cost. Plastic propellers are arguably the most common in amateur configurations. Remember: if you are still testing + and calibrating your drone, then go for cheap plastic propellers (they will get damaged for sure after a few bumps!), + and only use more expensive propellers once you have calibrated the vehicle and mastered the control. + Protective shields for propellers are usually also a *VERY* good idea. + +- **Brushless motors**: Smaller drones may sometimes opt for brushed motors, but these usually have lower efficiency + (75-80% vs. 85-90% for brushless motors) and tend to have a shorter life. However, brushless motors are usually more + expensive. The principle of a brushless motor is relatively simple: it consists of two parts, a _rotor_ (the rotating + part), and a _stator_ (the stationary part at the center of the motor). Both the stator and the rotor usually include + multiple permanent or coiled magnets. An alternate electric current applied to the stator generates a magnetic field, + and such magnetic field causes a misalignment between the magnetic fields of the stator and the rotor, continuously + attracting or repelling the coils in order to adjust the misalignment, and therefore resulting in a spinning movement. + +![Brushless motor structure](../img/brushless-motor-1.png) + +You can find many brushless motors for quadcopters online. There are a few things to keep an eye on when you select your +motors. First, the motor size, expressed as diameter and height of the stator (e.g. 2207 means that the stator is 22mm +wide and 7mm tall): bigger motors result in higher torque but they also absorb more power. Then, the _kV_ of the motor, +which expresses the ideal number of rotations for each volt applied. The _thrust-to-weight ratio_ expresses how much +thrust the motor can generate for each unit of weight - the higher, the better, 2:1 is an acceptable minimum value, 4:1 +is considered a middle sweet spot, high ratios such as 8:1 or 13:1 can theoretically be achieved by high-performance +motors, but after a certain number of rotations per second spinning a motor any faster is considered inefficient. +Finally, you may want to look at the advised number of LiPo cells for the motor. Since high-speed motors drain +more power, they usually require batteries with a higher number of cells, as a higher number of cells translates in a +higher voltage. + +- **Electronic Speed Controllers** (**ESCs**), one for each motor. Brushless motors work thanks to a varying current + that generates a rotating magnetic field, but you usually need a component between your controller and the motors + to transform the desired motor speed into an alternate current configuration to be applied to the motors. ESCs are + small circuits that do exactly this job. A factor to take into account when choosing an ESC is its current + specification (in Amperes). That expresses the maximum current that the ESC can deliver to the motors. Make sure that + it is higher than the current absorption you have estimated for your drone and lower than the current defined by the + discharge rate of your battery. + +- A **PWM servo controller**: Most of the ESCs out there communicate over _pulse width modulation_ (_PWM_). This is a + quite efficient way to transmit analog signals using the duration (width) of a digital signal. PWM basically allows + you to send analog signals using only one digital PIN. We will explore its internals a bit more in depth in the + coding section. Unfortunately, even though the Raspberry Pi theoretically has 4 PWM PINs, each of the pairs shares a + PWM resource (GPIO 12 and GPIO 18 share a PWM channel while GPIO 13 and GPIO 19 share the other channel). This means + that you can actually send a maximum of two distinct PWM signals at the same time, while our quadcopter obviously + needs four of them. To solve the problem, you can use a PWM servo controller to extend the PWM capabilities of the + Raspberry Pi - a popular choice is the [Adafruit 16-channel PWM driver](https://www.adafruit.com/product/815), which, + as the name suggests, can control up to 16 independent PWM channels at the same time, and you can even connect up to + 62 of these boards to control up to 992 channels, just in case you are planning to design a spaceship. + +- A pair of **batteries**. I said _pair_ because my advice is to use two separate power sources: a high-power LiPo + battery to power the motors, and a small LiPo battery to power the Raspberry Pi and the electronics (a Raspberry Pi + Zero that only runs the drone code shouldn't take much power). Even if a high-power LiPo can theoretically provide + enough power both for the motors and for the electronics, in reality the motors can suck up a lot of juice when they + spin fast, and if the current drops below a certain threshold the Raspberry Pi will just reboot (and you don't want + that to happen while your drone is hovering midair). You may also need a LiPo-to-USB power converter like the + [PowerBoost](https://shop.pimoroni.com/products/powerboost-1000-charger-rechargeable-5v-lipo-usb-boost-1a-1000c), + whose job is both to provide a USB output from a LiPo source and to stabilize the output voltage and current draw. + Keep in mind the previous consideration when you pick the battery to power the motors (weight, charge, discharge rate, + number of cells and output voltage). + +- A **power distribution board**, like [this one](https://www.amazon.de/-/en/gp/product/B071NXZLBM/). First, LiPo + batteries for quadcopters have specific connectors (usually XT60 or T-Plug) and you need some kind of adapter to + actually connect load to them. Second, these boards are a compact solution to distribute the power of a battery to + multiple loads - the board linked above can split the power of a LiPo battery to up to 6 loads. + +- A **controller**: Most of the commercial quadcopters have radio controllers, but that would add up both to the cost + and to the complexity of the circuitry used for the drone. For simplicity, in this article I'll illustrate how to set + up any device that exposes a joystick interface on Linux as your controller, with a particular focus on a Bluetooth + joystick (Bluetooth can only operate up to 10 meters, but since the Raspberry Pi has a built-in Bluetooth chip it + doesn't require USB dongles nor extra RC circuits). + +- A **drone frame**: You can find many drone frames online for quite affordable prices, or you can easily download a 3D + model and print it yourself. Personally I have used + [this model](https://cults3d.com/en/3d-model/various/drone-fpv-280) for my prototype, but + [this](https://cults3d.com/en/3d-model/game/chassis-drone-quadcopter) and + [this](https://cults3d.com/en/3d-model/gadget/drone-qav-250-fpv) are also popular (and more versatile) form factors. + +- A **camera** to take your amazing pictures and videos from above. We'll use a Raspberry Pi Camera in this tutorial, + since it already comes with a RPi native interface, and it doesn't require extra USB dongles and cables. + +- (_Optional_) an **accelerometer**: if you want to bring your drone to the next level then you can also add an + accelerometer to the Raspberry Pi. The accelerometer measures the gravity acceleration along the three axes. You can + easily add some logic to stabilize a drone if the input data from the accelerometer shows that it's leaning too much + in one direction. + +- (_Optional_) a **distance sensor**: ultrasound, laser or lidar sensors are very useful to study the environment around + the drone. You can place one at the bottom of the drone to detect when the drone has touched the ground, or some + sensors around the body to detect and avoid possible obstacles. + +## Putting things together + +You should now have a clear idea both of how to get a vehicle to lift and which components you need in order to achieve +it. Let's put together the ingredients covered in the previous section to show a possible schema for the connections of +your components. + +![Drone schema](../img/drone-schema.png) + +A couple of recommendations before jumping in a step-by-step breakdown of the diagram above: + +- Once you have all the components required for your drone, and before starting connecting, soldering and gluing things + together, get a scale, put all the components on it and check the total weight. This is the perfect moment to run the + calculations shown previously to estimate thrust, power and flight duration of your drone, now that the total mass is + known. + +- Play a bit with the placement of your components on the drone frame before you start connecting them together. Given + the shape of your frame, study what's the best placement of all the components - Raspberry Pi, PWM driver, camera, + power board, batteries, adapters, etc. Make a drawing of their placement, if it helps you remember a particular + configuration. Also, study how each component would connect with the others given their placement - the last thing you + want is an unmanageable ball of wires going all over the place. Space is limited on the body of a drone, and an + optimal initial arrangement of all the components puts your project in a good place. + +- Also, make sure that the weight is more or less balanced. It doesn't have to be 100% balanced on all the four arms, a + few grams of difference are still ok, the thrust can be adjusted accordingly during the calibration phase. However, a + too heavy imbalance can only be fixed by spinning some propellers at very high speeds, resulting in a higher current + drain, shorter duration of the flight, earlier damage to the motors and a harder to control drone. + +With these recommendations out of the way, let's dive into assembling our drone. + +### Frame + +Start by mounting the frame. Depending on the frame you bought or printed, this can be a task with a varying level of +challenge. When you screw the parts together, remember that plastic screws are lighter than metallic ones. Avoid using +hot glue to keep things together: you can use hot glue to stick other components to the frame or hold wires together, +but make sure that the frame is sturdy and strong, and that it holds together even without glue. Your drone may fall +a lot, and the last thing you want is the drone's body coming completely apart after a small fall. + +Once the frame is installed, motors are usually the next logical step. **ONLY** mount the motors right now, **NOT** the +propellers. Propellers should be installed only when you have managed to calibrate and remotely control the motors, and +you are sure that the drone won't flip backwards (or, worst, in your face) once you start giving power to the motors. + +### Motors and propellers + +If you have paid of attention to the previous diagram, you may have noticed that the motors spin in different +directions. More precisely, the motors on the opposite diagonals spin in the same direction, and those on the same side +spin on opposite directions. The most popular configuration is the following: + +- _Front-left motor_: clockwise spin. +- _Front-right motor_: anti-clockwise spin. +- _Back-left motor_: anti-clockwise spin. +- _Back-right motor_: clockwise spin. + +The reason for this configuration can be found in Newton's 3rd-law, a.k.a. "action and reaction". + +If a helicopter only had the top propeller and not the smaller propeller on the back, it simply won't fly. Its body +will just spin in the opposite direction as the rotation of its propeller. It's the propeller on the back (and the +length of the arm that connects it to the body) that balances the spinning movement and provides direction to the +vehicle. + +Similarly, if all the motors of a drone spun in the same direction then the drone will simply spin in the opposite +direction as their rotation. This is because the motors apply a _torque_ to the body of the drone, and the body of +the drone responds to this force with a forces that pushes it in the opposite direction. If instead you use an X-shaped +configuration, then the rotational components of the four torques cancel each other if they rotate at the same speed, +and all you have left is the lift. However, you can still adjust the torques during the flight to temporarily produce a +rotation (or _yaw_) of the drone around its _z_ axis. + +Most of the sets of motors for quadcopters already come in pairs that rotate in different directions. The direction of +the rotation is usually reported as a small arrow on the side of the motor's body. + +Once you have identified the correct spin direction of the motors, install the motors on the body of the drone. + +### Propellers direction, wind direction and lift + +Before proceeding with the other components, we should probably spend two words on the direction of the propellers +(again, **DON'T** install them yet, but just keep these considerations in mind when you are ready to install them). + +Propellers for quadcopters usually come in two pairs with wings facing in slightly different directions. If you look at +the sections of a set of propellers, you'll notice that they come with two different profiles: they either have the high +edge on the left side and the bottom edge on the right side, or the other way around. + +We previously mentioned that lift is generated by creating a pocket of fast-moving, low-pressure air on top of the wing +opposed to slow-moving, high-pressure air on the bottom. The high-pressure air below the wing ends up applying an +upward facing force that causes the vehicle to lift. + +![Propeller rotation vs. air direction](../img/propeller-schema.png) + +If you look at the section of a propeller and picture it against the diagram above, then it's quite simple to figure +out which propellers need to be attached to which motors. The propellers should "cut" through the air through the side +that faces upwards. Because of Newton's 3rd law, air will start moving in the direction opposite to the spin direction +of the motor. This causes the air above the blade to "jump" up, flow downwards along the blade, and eventually apply a +higher pressure to the air below it. Therefore, a propeller with the "high" edge on the left of its section needs to be +mounted on a clockwise spinning motor, and a propeller with the "high" edge on the right should be mounted on a +counter-clockwise spinning motor. If you invert the direction, then no matter how fast you spin the blades, your drone +won't move by an inch. That's because, under such configuration, the upper face of each blade would move the air up, +therefore the moving air would apply no lift force to the structure of the propeller, and all you have built is a +powerful fan to keep you cool in summer. + +### Setting up the remaining hardware + +Once the motors are set up, it's time to glue the ESCs to the frame of the drone and connect the motors to them. + +An ESC usually has three wires or soldering pads on one side and five wires on the other. The side with three +wires/soldering pads should face the motor and be connected to the three wires coming out of the motor. These +wires work as a current tri-phase system with each phase shifted by 120 degrees compared to the others. The shift in the +output currents is what generates the alternate current that generates the rotating electromagnetic field that causes +the motor to spin. It shouldn't matter in which order you solder these wires to the motor. + +![Example of an ESC](../img/esc-1.jpg) + +On the other side you will usually find a red wire and a black wire, respectively the reference VCC and GND, which need +to be connected to the battery through the power distribution board. + +Finally, you usually find three additional wires on the same side, usually bundled together in a plastic connector and +marked by black, red and white colors. These wires need to be plugged to the PWM circuit board, the red and black +cables must be respectively be plugged to the VCC and GND PINs of the board and the white one to the PWM signal PIN. + +After you have installed all the four ESCs and connected them to the motors, it's time to install the power supply +board and connect the ESCs to it. + +![Power supply board](../img/xt60-board.jpg) + +Most of the boards for quadcopters have two pairs of power supply connectors for the ESCs respectively on the left and +right side. Solder the red wire of each ESC to a _+_ connector and each black wire to the _-_ connector. + +After you have mounted the power supply board and soldered the ESC power connectors to it, it's time to move to the +PWM servo board that will control the PWM signals sent to the motors. + +![Adafruit 16-channel PWM board](../img/adafruit-16-pwm.jpg) + +There may be quite a bit of PINs to solder if you are using the Adafruit 16-channel board. You have to at least solder +the following: + +- At least one header of PINs on either the left or right side of the board. These PINs are used to communicate with + the Raspberry Pi over I2C interface, so the only connections you really need are VCC (to the Raspberry Pi 3.3V or 5V + PIN), GND (to any of the Raspberry Pi ground PINs), SDA (serial data, to the Raspberry Pi I2C SDA PIN, usually GPIO2) + and SCL (serial clock, to the Raspberry Pi I2C SCL PIN, usually GPIO3). + +- Four triples of PINs (one per ESC) on the PWM connectors on the bottom side. You will connect the wires from the + ESC to these PINs - white wire to PWM, red wire to V+ and black wire to GND. + +- Two PINs or a screwed adapter on the V+/GND connectors on the top of the board. You may want to connect the power + supply board 12V/GND connectors here, since most of the ESC devices usually operate around a 9-16V voltage. + **NEVER** connect the V+ line to the Raspberry Pi in any way! 3.3V is supposed to be the maximum voltage of any + circuitry connected to the Raspberry Pi. If any higher voltage is present, then the current may start flowing on + wrong paths and you may end up damaging your Raspberry Pi for good. So connect the V+ to the power supply board, + but make sure that no other V+ PIN is connected to the Raspberry Pi - the only voltage connection to the Raspberry Pi + must be through VCC, which should always be in the 3.3-5V range. + +- It's also advised to solder an electrolytic capacitor on the top-left area of the board marked by _C2_. The + electrolytic capacitor provides a voltage buffer to the connected components in case of sudden current spikes or + drops. + +Once the PWM circuitry is set up, you can move to gluing or screwing the Raspberry Pi Zero. Connect the I2C interface +on the side of the Adafruit board to the Raspberry Pi. If we consider the following pinout diagram: + +![Raspberry Pi pinout](../img/rpi-pinout.jpg) + +Then you'll need the following connection: + +- PWM board VCC PIN: connect to PIN 1, 2, 4 or 17 (the Adafruit 16-channel PWM board has an operative voltage range of + 3-6V). + +- PWM board GND PIN: connect to PIN 6, 9, 14, 20, 25, 30, 34 or 39. + +- PWM board SDA PIN: connect to PIN 3 (GPIO 2/I2C SDA). + +- PWM board SCL PIN: connect to PIN 5 (GPIO 3/I2C SCL). + +If you want, you can connect a camera now - simply plug the ribbon cable into the slot on the right side of the board +if you are using a Raspberry Pi compatible camera. + +Once the Raspberry Pi is mounted and connected to the rest of the circuitry, you can finally proceed with the +installation of the batteries. Mount the large LiPo battery on the frame and connect its XT60 or T-Plug cable to the +power supply board. If you got all the connections right, you should see both the PWM board and the ESCs power up. + +Proceed with the installation of the smaller LiPo battery for the Raspberry Pi. You can use any available LiPo battery +shield or adapter available for the Raspberry Pi, there are many options available to power the Raspberry Pi from a LiPo +either over a USB adapter or through a shield/hat. + +Once everything is connected, it's time to move to the software side of the project. + +## Prepare the Raspberry Pi + +First, you'll need to flash a Linux image to an SD card. Any distro should work, but this article will mostly focus on +Raspbian/Raspberry Pi OS because it makes the CircuitPython setup much easier. + +[Download a Raspberry Pi OS image](https://www.raspberrypi.org/%20downloads/). I personally used a headless version +(i.e. with no X server nor desktop environment) because you don't really need to plug an HDMI cable, a mouse and a +keyboard that often into a drone and use it like a computer, but if that's one of the use cases that you have in mind +feel free to get a full desktop image. Use any of the supported methods to flash the downloaded image to the SD card, +depending on the host operating system. For instance, the quickest way from Linux would be to simply use `dd` +(**ALWAYS** make sure that the `/dev` file specified actually points to your SD card, or you may risk wiping your own +filesystem): + +```shell +$ sudo dd if=./raspberry-pi-os-VERSION.img of=/dev/mmcblk0 bs=8M conv=fsync status=progress +``` + +Wait a bit (depending on the size of the SD card) and once the copy is done unmount the device and place it into the SD +card slot on the Raspberry Pi. Before booting the device, connect it to a monitor over an HDMI cable and connect a +wireless/wired keyboard - this may be the only time that you need to actually need to connect the Raspberry to a screen. +Boot the device and wait for the partition resize process to complete. You should eventually be prompted to a login +screen - use the default credentials, _pi - raspberry_, to log in. + +We may now want to set up Wi-Fi connection, I2C and camera interfaces and SSH access for remote control. This can be +easily done on Raspberry Pi OS through the `raspi-config` utility: + +- Type `sudo raspi-config`. + +- Select `System Options` -> `Wireless LAN` and enter the details of your Wi-Fi network. + +- Go back, select `Interface Options` -> `SSH` and enable SSH access. + +- It's also a good idea to set up auto-login with the `pi` user, so a user session with all the required scripts and + services will start automatically once the device is booted. This can be done from `System Options` -> + `Boot / Auto Login` -> `Console/Desktop Autologin`. + +- The Adafruit PWM servo communicates over I2C interface, therefore make sure it's enabled on the Raspberry Pi + (`Interface Options` -> `I2C`). + +- If you are using a Raspberry Pi camera, you can enable it from here (`Interface Options` -> `Camera`). + +- Select `Finish` and reboot. + +- On reboot type `ifconfig` or `ip addr` to verify that you are connected to the Wi-Fi network and note down the + assigned IP address (you may also want to add a static MAC rule on your router to make sure that it stays the same + across reconnections). + +Once your device is online, you can unplug the HDMI connection and any mouse or keyboard and restart the Raspberry Pi. +After a few seconds you should be able to ping its IP address, and you can SSH into it from your desktop/laptop through +`ssh pi@rpi-ip`. + +Once you have your remote shell to the drone, update it and make sure that the basic Python dependencies are available: + +```shell +$ sudo apt update +$ sudo apt upgrade +$ sudo apt install python3 python3-pip +``` + +You should now proceed with installing the CircuitPython environment. The +PCA9685 chip used by the Adafruit 16-channel PWM board is only compatible with CircuitPython (a minimal installation +of the Python interpreter dedicated to IoT and low-power devices), which isn't installed on the Raspberry Pi by default. + +Official instructions are +[provided on the Adafruit website](https://learn.adafruit.com/16-channel-pwm-servo-driver?view=all#python-circuitpython). +The quickest way if you are running Raspbian/Raspberry Pi OS is probably to install the Adafruit Python shell, download +and run the `blinka` script: + +```shell +$ sudo pip3 install --upgrade adafruit-python-shell +$ wget https://raw.githubusercontent.com/adafruit/Raspberry-Pi-Installer-Scripts/master/raspi-blinka.py +$ sudo python3 raspi-blinka.py +``` + +Wait a bit for the script to install all the required dependencies, then reboot the device. If everything was +successful, upon reboot you should see a new direct-access block device under `/dev/i2c-1`. You can now install the +CircuitPython driver for the PCA9685 chipset: + +```shell +$ sudo pip3 install --upgrade adafruit-circuitpython-pca9685 +``` + +## Set up the controller + +Time to move to the controller for our drone. As mentioned earlier, my choice for prototyping is a Bluetooth controller, +since the Raspberry Pi Zero already comes with a built-in Bluetooth adapter and connecting it doesn't require any extra +wires nor dongles. However, Bluetooth poses a limit on the maximum distance between the drone and the controller, since +Bluetooth signals quickly degrade after about 10 meters, and on even shorter distances if there are some obstacles +between the sender and the receiver. My advice is therefore to pick Bluetooth in the initial design and prototyping +phases, or if you plan to fly the drone mostly indoor or on short distances. If instead you plan to fly your drone more +than 10 meters away from you, then you may want to opt for a proper radio controller like +[this](https://www.amazon.com/Radiolink-Transmitter-Controller-Multicopters-Helicopter/dp/B07FPF2HQR/). However, a +proper radio set up is usually expensive, and you'll also have to install and configure a radio receiver on the +Raspberry Pi as well. Moreover, if you are planning to fly your drone outdoor while viewing its camera feed then you +may also want to install a 3G/4G SIM module on the drone, as well as a GPS sensor in order to locate it. These are +eventually the most expensive parts that impact the final price of high-range drones, and they are going to be expensive +even if you go for the DIY way. So my advice is to make a first prototype using the Bluetooth controller on short range, +and once you are confident about your creature then you can make the investment and buy 3G/4G, GPS and RC hardware for +a proper remote control and interface. + +Sticking to the Bluetooth controller case, first turn it on and put it in pairing mode (instructions differ from device +to device, consult the manual of the controller). Once the controller is in pairing mode, start the Bluetooth +administration service on the Raspberry Pi and connect the controller: + +```text +$ sudo bluetoothctl +[bluetooth]# scan on +... +[NEW] Device 00:11:22:33:44:55 My Joystick +... +[bluetooth]# scan off +[bluetooth]# pair 00:11:22:33:44:55 +[bluetooth]# connect 00:11:22:33:44:55 +[bluetooth]# trust 00:11:22:33:44:55 +[bluetooth]# exit +``` + +If the connection was successful you should see a new joystick device under `/dev/input/js0`. You can test that all +the buttons are correctly detected through the `jstest` utility: + +```shell +$ jstest /dev/input/js0 +``` + +If you can't access the device through the `pi` user then check its permissions and groups through +`ls -l /dev/input/js0`. If it has no read privileges for non-group users then simply add the `pi` user to the +associated group (usually `input` on Raspberry Pi OS) and reboot the device. + +## Connect the pieces together + +Now that all the hardware is in place and connected, we need to program the "brain" of the drone - the software +component that emulates the flight controller, reads commands from the joystick and forwards them to the motors as +PWM signals. + +We'll use [Platypush](https://platypush.tech) to connect the pieces together, since it comes both with a plugin for the +[PCA9685 chipset](https://docs.platypush.tech/platypush/plugins/pwm.pca9685.html) and +[a backend](https://docs.platypush.tech/platypush/backend/joystick.linux.html) to read events from joysticks and joypads +on Linux, as well as several plugins and backends to stream camera feeds. + +Install Platypush on the Raspberry Pi Zero together with the web server, PCA9685 and PiCamera dependencies: + +```shell +$ sudo pip3 install --upgrade 'platypush[http,picamera,pca9685]' +``` + +Then create a simple configuration file under `~/.config/platypush/config.yaml`: + +```yaml +backend.http: + # Listen port + port: 8008 + +backend.joystick.linux: + device: /dev/input/js0 + +camera.pi: + horizontal_flip: False + vertical_flip: False + resolution: + - 800 + - 600 + +pwm.pca9685: + # PWM main frequency, in Hz + frequency: 500 + + # Default PWM channels to control. + # In this case, we have connected the ESCs + # to the first four channels on the board. + channels: + - 0 + - 1 + - 2 + - 3 + +# Start streaming the camera feed on TCP port 5000 +# when the application starts +event.hook.OnApplicationStarted: + if: + type: platypush.message.event.application.ApplicationStartedEvent + then: + - action: camera.pi.start_streaming + args: + listen_port: 5000 +``` + +All the sections of the file should be relatively self-explanatory, but the PCA9685 configuration requires a deeper dive +in how PWM modulation works under the hood to be properly grasped - we'll dive into it very soon. + +In the meantime, you should already be able to start the application through the `platypush` command +(use the `pi` user). If everything was installed and configured properly, after a while you should see the camera +sensor turn on - you can view the camera feed either through the +[RPi Camera Viewer](https://play.google.com/store/apps/details?id=ca.frozen.rpicameraviewer&hl=en_US&gl=US) app for +Android, or through VLC: + +```shell +$ vlc tcp/h264://rpi-ip:5000 +``` + +If you turn on and pair your Bluetooth controller you should also be able to see some events in the application log +when you press some controls on the device: + +``` +... +INFO|platypush|Received event: {"type": "event", "args": {"type": "platypush.message.event.joystick.JoystickConnectedEvent", "device": "/dev/input/js0", "name": "PG-SW021", "axes": ["x", "y", "z", "rz", "gas", "brake", "hat0x", "hat0y", "x", "y", "z", "rz", "gas", "brake", "hat0x", "hat0y", "x", "y", "z", "rz", "gas", "brake", "hat0x", "hat0y", "x", "y", "z", "rz", "gas", "brake", "hat0x", "hat0y", "x", "y", "z", "rz", "gas", "brake", "hat0x", "hat0y"], "buttons": ["a", "b", "c", "x", "y", "z", "tl", "tr", "tl2", "tr2", "select", "start", "mode", "thumbl", "thumbr", "a", "b", "c", "x", "y", "z", "tl", "tr", "tl2", "tr2", "select", "start", "mode", "thumbl", "thumbr", "a", "b", "c", "x", "y", "z", "tl", "tr", "tl2", "tr2", "select", "start", "mode", "thumbl", "thumbr", "a", "b", "c", "x", "y", "z", "tl", "tr", "tl2", "tr2", "select", "start", "mode", "thumbl", "thumbr", "a", "b", "c", "x", "y", "z", "tl", "tr", "tl2", "tr2", "select", "start", "mode", "thumbl", "thumbr"]}} +... +INFO|platypush|Received event: {"type": "event", "args": {"type": "platypush.message.event.joystick.JoystickAxisEvent", "device": "/dev/input/js0", "axis": "hat0x", "value": -1.0}} +INFO|platypush|Received event: {"type": "event", "args": {"type": "platypush.message.event.joystick.JoystickAxisEvent", "device": "/dev/input/js0", "axis": "hat0x", "value": 0.0}} +... +``` + +If you see these events it means that the joystick was successfully detected. We'll come back to these events in a bit +to see how to connect them to drone actions. It is now a good idea to configure Platypush as a startup service for the +`pi` user, so whenever a new user session starts the application will also be started. Get its absolute path: + +``` +$ which platypush +``` + +Then create a new systemd service file as `~/.config/systemd/user/platypush.service` with the following content: + +``` +[Unit] +Description=Platypush service +After=network.target bluetooth.target + +[Service] +ExecStart=/path/to/platypush +Restart=always +RestartSec=5 + +[Install] +WantedBy=default.target +``` + +Then reload the systemd daemon, start and enable the service: + +``` +$ systemctl --user daemon-reload +$ systemctl --user start platypush.service +$ systemctl --user enable platypush.service +``` + +It's now time to dive into the PWM internals to understand how to _program_ the controls for the motors through the +Raspberry Pi. + +## Pulse-Width Modulation (PWM) explained + +PWM is one of the most common ways of driving electric motors because it makes it easy to transmit analog values using +only one digital signal that can either be high or low, and it optimizes power consumption by applying a voltage to the +load only when a change to the output power is required. The _duration_ of the high part of the signal expresses the +value that you want to transmit, therefore the longer the duration of the high part of a signal, the higher the voltage +or current that will be transmitted to the load. Conceptually, PWM is somehow similar to popular radio modulation +technologies such as AM or FM, but instead of modulating information through the amplitude or the frequency of the +signal, it modulates it through the duration of a digital high signal. + +For example, the figure below shows how to transmit a sinusoidal signal _B_ through a PWM train of pulses _V_. The +duration of each pulse expresses how much the output signal should go up (if the voltage is positive) or down (if the +voltage is negative) compared to the previous output value. The duration of a high pulse is also called _duty cycle_. + +![PWM signal example](../img/pwm-1.png) + +The most important metric to take into account when designing PWM systems is the base frequency of the modulation +process - in other words, how often the signal should be sampled. When driving motors that usually have a minimum and +maximum power, you may also want to specify the minimum and maximum _duty cycle_, i.e. the minimum and maximum duration +of a high pulse that will be respectively be mapped to the minimum and maximum output power. It is very important to +pick these values in such a way that the minimum value is greater than the sampling period (which is simply the inverse +of the sampling frequency). To be more specific, according to +[Nyquist-Shannon's sampling theorem](https://en.wikipedia.org/wiki/Nyquist%E2%80%93Shannon_sampling_theorem), the +minimum frequency used to deliver actual information must be at least twice the sampling frequency. If that's not the +case then some pulses will not be detected, because the duration of the pulse may be shorter than the sampling period. + +Most of the ESC controllers on the market have their own supported PWM configurations, and some advanced controllers may +even allow more complex controls over PWM - for example for controlling sounds, lights or supporting multiple motor +control paradigms. Therefore, the user manual of your ESC (or the chipset used by your ESC) is usually the best place +to start before writing the code that actually controls the motors. + +In my case I have used some ESC devices with a +[BLHeli_32 ARM chipset](https://github.com/bitdump/BLHeli/tree/master/BLHeli_32%20ARM), a quite popular option for +mid-to-high tier quadcopters - and also a very versatile one, since it's basically a small programmable 32-bit ARM +microcontroller. The +[user manual](https://render.githubusercontent.com/view/pdf?color_mode=light&commit=746220a3da8b36a37b9388fec624bc50299ce974&enc_url=68747470733a2f2f7261772e67697468756275736572636f6e74656e742e636f6d2f62697464756d702f424c48656c692f373436323230613364613862333661333762393338386665633632346263353032393963653937342f424c48656c695f333225323041524d2f424c48656c695f33322532306d616e75616c25323041524d25323052657633322e782e706466&nwo=bitdump%2FBLHeli&path=BLHeli_32+ARM%2FBLHeli_32+manual+ARM+Rev32.x.pdf&repository_id=3813420&repository_type=Repository#c0f6bc3e-8870-4047-8084-2b0cafc3cfbd) +mentions support for a regular pulse width input between 1-2ms. We can pick 2ms as our sampling period, and +\(1/2ms = 500 Hz\) will therefore be our sampling frequency - configured in the Platypush `pwm.pca9685` plugin through +the `frequency` attribute. The manual also mentions support for other PWM configurations (such as OneShot125, OneShot42, +Multshot and Dshot) that usually rely on shorter pulses, but for sake of simplicity (and compatibility with other ESC +devices) we can stick to the regular 2ms sampling period. + +Also, the driver of the PCA9685 controller automatically takes care of picking the right duty cycle range given +the frequency, and it maps the duty cycle to 16-bit integers between 0 and 65535. A duty cycle of zero means 0% of the +maximum power, a duty cycle of 0xffff means 100% of the power, a duty cycle of 0x7fff means 50% of the power. The +Platypush wrapper for the PCA9685 uses `min_duty_cycle=0` and `max_duty_cycle=0xffff` unless configured otherwise, but +this is probably not what you want for your drone. You usually want a higher minimum duty cycle because under a certain +threshold the power will be insufficient to even get the motors to spin. And, in most of the cases, you don't want to +have a maximum duty cycle that is 100% of the maximum power. If you have 35A ESC, that will draw 35A from the battery +and, most importantly, it will probably turn your drone into an uncontrollable killer machine. In a general use case you +may want to identify the minimum duty cycle associated to a minimal spin of the motors and pick that as the minimum duty +cycle, and identify the duty cycle associated to a lift-off at the desired speed and pick that as the maximum duty +cycle. We'll see how to use Platypush to calibrate this range through the joystick in a bit. + +Finally, most of the ESC devices have an **arming** sequence. Its main purpose is to prevent unexpected currents or +accidental controls from suddenly spinning the motors when it's not desired/required. Therefore, before flying the drone +you need to actually send a special PWM sequence that _arms_ the ESC controllers, and sometimes you may need to send +another sequence to disarm them (this is usually achieved by bringing the power back to zero, and most of the ESC would +automatically disarm after a while if no further pulses are sent). + +You should consult the user manual of your ESC to check the supported arming and disarming sequences. For examples, +the BLHeli_32 user manual describes the following procedure: + +![BLHeli_32 arming sequence](../img/esc-pwm.png) + +What this diagram says is that: + +- The ESC should beep three times when it's powered on. + +- Once the ESC is powered on, the arming sequence can be initiated by gradually increasing the voltage on the PWM + input from 0 to approximately 50% the maximum duty cycle, and then bringing the voltage down back to zero. + +- When the input voltage hits the first low voltage threshold, the ESC emits a first low beep. When it goes back to + zero, the ESC emits a high beep. + +- If you hear both the beeps it means that the arming sequence was correct, and you can start stepping up the voltage + to actually send power to the motors. + +- If you only hear the low beep, it means that the arming sequence hasn't been successful. It usually means that the + pulse train was either too long or too short - its duration should be a multiple of the sampling period, but if it's + too much higher then the arming sequence will time out. + +- The deactivation sequence simply consists in bringing the duty cycle to zero and de-initializing the PCA9685 driver. + +Now that we have learned the theory of what PWM is, how to use it for signal transmission protocols and how to arm and +deactivate an ESC, it's time to get our hands dirty with some code. + +## Coding the flight controller + +Also, be aware: this is the part where you actually spin and test the motors. Once again, **do it without the +propellers**, and double check again that all the connections to the motors are properly set up and that the ESCs are +powered on before you try and send any signal. + +Create a new script under `~/.config/platypush/scripts/drone.py`. This script will contain the automation to control +your drone through the joypad. We first need to code a function to arm the four ESC devices. If you are using a +BLHeli_32-based ESC (or any other ESC device with a double-ramp arming sequence) then you may want to code a function +that uses the `pwm.pca9685` plugin to write a zero to the PWM channel, wait a bit, write a value about half of the +maximum duty cycle, wait a bit, and write another zero. And we also need a function that resets the motors by bringing +the voltage to zero and resetting the PCA9685 driver: + +```python +import logging +import time + +from platypush.context import get_plugin + +logger = logging.getLogger(__name__) + +def arm_motors(): + logger.info('Arming drone ESCs') + pwm = get_plugin('pwm.pca9685') + pwm.write(0) + time.sleep(0.5) + pwm.write(0x7fff) + time.sleep(0.5) + pwm.write(0) + time.sleep(0.5) + logger.info('Drone ESCs ready') + +def reset_motors(): + logger.info('Resetting drone ESCs') + pwm = get_plugin('pwm.pca9685') + pwm.write(0) + pwm.reset() + pwm.deinit() + logger.info('Drone ESCs reset') +``` + +The right time between the writes largely depends on the arming sequence supported by the ESC and it may be a bit of a +trial-and-error process. Many ESCs will emit a particular sequence of beeps or flash their LED in a particular way upon +successful arming sequence. Before you add more code or hook these events to joystick events, open a Python CLI from +the `~/.config/platypush` directory and give your logic a test: + +```python +>>> from scripts.drone import arm_motors, reset_motors +>>> arm_motors() +# Arming drone ESCs +# Drone ESCs ready +>>> reset_motors() +# Resetting drone ESCs +# Drone ESCs reset +``` + +In this case we are not specifying the list of PWM channels to write: if a single value is specified on the `.write()` +method then the same value will be written to all the channels configured on the plugin, and we previously configured +the `pwm.pca9685` plugin to use the first four channels by default. + +Once you have managed to arm and disable the ESCs, let's associate these functions to joypad events. For instance, we +can prepare the motors when the joypad connects to the drone and disable them when the joypad disconnects. This can +be easily done in the `drone.py` script by leveraging the Platypush +[`JoystickConnectedEvent`](https://docs.platypush.tech/platypush/events/joystick.html#platypush.message.event.joystick.JoystickConnectedEvent) +and [`JoystickDisconnectedEvent`](https://docs.platypush.tech/platypush/events/joystick.html#platypush.message.event.joystick.JoystickDisconnectedEvent): + +```python +from platypush.event.hook import hook +from platypush.message.event.joystick import JoystickConnectedEvent, JoystickDisconnectedEvent + +@hook(JoystickConnectedEvent) +def on_joystick_connected(**_): + arm_motors() + +@hook(JoystickDisconnectedEvent) +def on_joystick_disconnected(**_): + reset_motors() +``` + +Now restart the service on the drone: + +```shell +$ systemctl --user restart platypush +``` + +Then start/pair/connect the joypad. Upon connection, you should see a `JoystickConnectedEvent` on the logs and the ESCs +should automatically get armed. Now it's a good idea to set up a joystick button to trigger the `.reset_motors()` +and `.arm_motors()` methods. This will allow us to programmatically arm/disable the motors without having to +connect/disconnect the joypad. Press the joystick keys that you want to associate to the arm and reset actions while the +application is running and check the associated key code in the logs. For example, if you pick the _Select_ and _Start_ +buttons on a PlayStation or XBox-like joypad: + +```text +Received event: {"type": "event", "args": {"type": "platypush.message.event.joystick.JoystickButtonPressedEvent", +"device": "/dev/input/js0", "button": "select"}} +Received event: {"type": "event", "args": {"type": "platypush.message.event.joystick.JoystickButtonPressedEvent", +"device": "/dev/input/js0", "button": "start"}} +``` + +So create an event hook in your `drone.py` script that associates the `.arm_motors()` and `.reset_motors()` methods to +these buttons: + +```python +from platypush.event.hook import hook +from platypush.message.event.joystick import JoystickButtonPressedEvent + +@hook(JoystickButtonPressedEvent, button='start') +def on_start_button_pressed(**_): + arm_motors() + +@hook(JoystickButtonPressedEvent, button='select') +def on_select_button_pressed(**_): + reset_motors() +``` + +Now pick two joystick buttons that you want to use to bring the motor power respectively up and down and log the current +duty cycle, and add event hooks for them. For example, during calibration I usually use the L2/R2 buttons on the back of +the joystick to respectively bring the power down or up: + +```python +import logging + +from platypush.context import get_plugin +from platypush.event.hook import hook +from platypush.message.event.joystick import JoystickButtonPressedEvent + +logger = logging.getLogger(__name__) +# This defines how much we should take the motor power up or down on each step. +# Feel free to experiment different values for different levels of control. +step = 25 + +def power_down(): + pwm = get_plugin('pwm.pca9685') + channels = { + i: value-step + for i, value in pwm.get_channels().output.items() + } + + logger.info(f'Power down. Channel values: {channels}') + pwm.write(channels=channels) + +def power_up(): + pwm = get_plugin('pwm.pca9685') + channels = { + i: value+step + for i, value in pwm.get_channels().output.items() + } + + logger.info('Power up. Channel values: {channels}') + pwm.write(channels=channels) + +@hook(JoystickButtonPressedEvent, button='tl2') +def on_power_down_button_pressed(**_): + power_down() + +@hook(JoystickButtonPressedEvent, button='tr2') +def on_power_up_button_pressed(**_): + power_up() +``` + +Now restart the service and arm the ESCs. Once armed, press R2 repeatedly to bring up the power provided to the motors. +At the beginning the motors might not move at all, but once passed a certain current threshold they will start spinning +lightly. Take note of the power configuration required to achieve a minimum level of spin on the motors, it should now +be reported on the application logs. This is going to be the `min_duty_cycle` on the `pwm.pca9685` plugin configuration, +since any lower currents won't cause any change to the speed of the motors. Some motors may also have an upper boundary +for the maximum current that could be lower than the maximum current that can be delivered by the ESCs. If that's the +case, then the motors will usually automatically stop. If you notice such behaviour while powering up the motors, then +configure the associated duty cycle as `max_duty_cycle`. For example, this configuration seems to work quite well for my +combination of motors and ESCs: + +```yaml +pwm.pca9685: + frequency: 500 + min_duty_cycle: 2000 + max_duty_cycle: 4000 + channels: + - 0 + - 1 + - 2 + - 3 +``` + +Now that we have some commands configured to arm and reset the motors, and some logic to increase or decrease the power, +we can proceed to calibrating the drone. + +## Drone calibration + +This is the moment of truth of the project: you can now set up the propellers on the motors and verify that your +creature can move (follow the previous instructions about the direction and the order of the blades). + +However, before calling your friends to show off your new drone, you may consider calibrating the motors - and this +may be a quite time-intensive process. + +In theory, sending the same current to two motors should cause the same torque on the body of the drone. However, many +factors (such as small production differences between individual motors, distribution of the weight on the frame or +different level of friction between the stator and the rotor) usually cause the motors of a drone to behave slightly +differently. If the torques of the motors are not perfectly balanced, then the drone will either spin around its body or +flip in the direction opposite to the one with the strongest torque. Therefore, you may want to add to your `drone.py` +script a mapping of the current offsets for each motor. A smaller value means that less current will be provided to +that motor compared to the others, a greater value is the other way around. For now let's initialize this table with +zeros: + +```python +class Channel: + """ + Motor position to PWM channel index. + """ + TOP_LEFT = 0 + TOP_RIGHT = 1 + BOTTOM_LEFT = 2 + BOTTOM_RIGHT = 3 + +offsets = { + Channel.TOP_LEFT: 0, + Channel.TOP_RIGHT: 0, + Channel.BOTTOM_LEFT: 0, + Channel.BOTTOM_RIGHT: 0, +} +``` + +Now let's wrap the `.arm_motors()` method into a `.warmup()` method that does the following: + +1. Arms the ESCs. + +2. Increases the current sent to the motors by gradually increasing the duty cycle to each of the ESCs until + `min_duty_cycle + offset[i]`. This will get the motors to rotate at low speed. + +3. The joystick `start` button will call this `.warmup()` method instead of `.arm_motors()`. + +The logic will look like this: + +```python +import logging + +from platypush.context import get_plugin +from platypush.event.hook import hook +from platypush.message.event.joystick import JoystickButtonPressedEvent + +logger = logging.getLogger(__name__) + +def warmup(): + arm_motors() + logger.info('Starting the propellers!') + pwm = get_plugin('pwm.pca9685') + + # Start from 50% of min_duty_cycle + channels = { + channel: int(0.5 * pwm.min_duty_cycle) + offset + for channel, offset in offsets.items() + } + + pwm.write(channels=channels) + + # Bring the power up to min_duty_cycle + channels = { + channel: pwm.min_duty_cycle + offset + for channel, offset in offsets.items() + } + + pwm.write(channels=channels, step=15, step_duration=0.03) + +@hook(JoystickButtonPressedEvent, button='start') +def on_start_button_pressed(**_): + warmup() +``` + +Now it's time to remind a few precautions: + +- Make sure that there's enough room around the drone and that it's sitting on a large surface. + +- Make sure that no obstacles or people are present in a radius of at least 2-3 meters from the drone. + +- Remember that once the motors have the propellers attached and you start loading a lot of current into them, whatever + you attached to them will start moving. But you haven't calibrated the motors yet, so you _don't know_ yet in which + direction the drone will move. Behave accordingly. + +With your drone sitting in a safe spot, it's time to restart Platypush, trigger the `.warmup()` function and start +powering juice to the motors. At some point you'll hopefully start seeing the drone hover and slightly move in some +direction. This is the moment where you should pay most of the attention, for two reasons: + +1. You know have an uncalibrated hovering device with blades that spin hundreds of times a second, and you + know the rules: as soon as anything goes wrong, _hit that `.reset_motors()` button_! + +2. It's very likely that the drone won't simply move up straight from the floor at this stage, because some motors may + generate a lower or higher torque than others. + +3. Unless you have been very lucky on the point above, the first time you try and fly the drone it'll move in some + random direction. Therefore don't provide too much power to the motors - you don't want to fly it just yet, you + just want to let it hover a bit to see how the forces are balanced. + +Regarding the second point, pay attention to how the drone moves in order to understand how to calibrate it: + +1. If it spins clockwise, then the torque from the anti-clockwise motors is stronger than the torque from the motors + that spin clockwise. Slightly decrease the offsets of `TOP_RIGHT` and/or `BOTTOM_LEFT`. The opposite applies if the + drone spins clockwise. + +2. If it spins clockwise/anti-clockwise, but the rotation axis is closer to a motor instead of the center of the drone, + then the motor closer to the rotation axis generates a lower torque: increase its offset value. + +3. If it tilts towards the left, then the torque generated by the motors on the right side is stronger: reduce the + offsets values of `TOP_RIGHT` and `BOTTOM_RIGHT`. The opposite applies if it tilts on the left side. + +4. If it tilts towards the front, then the torque generated by the motors on the back is stronger: reduce the + offsets values of `BOTTOM_LEFT` and `BOTTOM_RIGHT`. The opposite applies if it tilts towards the back. + +Observe how the balance of the forces affects the movement of the drone, adjust the offset values accordingly, restart +the service. Repeat this procedure until you get the drone to lift it more or less on a straight line once a sufficient +level of power has been reached. + +Once you have found the combination of power offsets that causes your drone to lift along a straight line, reduce the +power pumped to the motors until the drone stabilizes into a hovering position. Take note of the values being sent to +the ESCs when static hovering is achieved, they will be reported on the application logs as `Power up/down. +Channel values: ` lines. These will be the values needed to achieve static thrust. Save them in your +`drone.py` script, we'll use them later as a reference to control the movement of the drone while it's flying: + +```python +static_thrusts = { + Channel.TOP_LEFT: ..., + Channel.TOP_RIGHT: ..., + Channel.BOTTOM_LEFT: ..., + Channel.BOTTOM_RIGHT: ... +} +``` + +Now that the most difficult part of the project (getting the drone off the ground on a straight line) is done, it's time +to move to programming the logic to actually move your drone while it's in the air. + +## The mechanics of flight control + +Once it's hovering above the ground, you can picture a drone as a body that can move in four possible ways. It can +go up/down depending on the power supplied to the motors, or it can rotate around its two horizontal axes (_x_ and _y_) +or around the vertical axis (_z_). + +1. The movement of a drone up or down along the vertical axis is called **throttle**. + +2. The rotation of a drone around the _x_ axis is called **roll**. + +3. The rotation of a drone around the _y_ axis is called **pitch**. + +4. The rotation of a drone around the _z_ axis is called **yaw**. + +![Pitch, roll and yaw schematics (credits: https://www.researchgate.net/publication/329392693_Autonomous_Person_Detection_and_Tracking_Framework_Using_Unmanned_Aerial_Vehicles_UAVs)](../img/pitch-roll-yaw-1.png) + +To regulate each of these movements from a stationary position: + +1. _Throttle_: simply increase/decrease by the same offset the power provided to each of the motors. Higher offsets + result in the drone moving up, lower values result in the drone moving down. + +2. _Roll_: you can use this movement to move the drone laterally without rotating its front-facing side. Slightly + increase the power supplied the motors on the opposite side of the desired direction and slightly decrease the + power supplied to the motors on the other side. So moving the drone to the left is achieved by increasing the power + to the `TOP_RIGHT` and `BOTTOM_RIGHT` motors and decreasing the power to the `TOP_LEFT` and `BOTTOM_LEFT` motors, + and the other way around if you want to move the drone to the right. + +3. _Pitch_: you can use this movement to move the drone forward or backwards. Again, slightly increase the power + supplied the motors on the opposite side of the desired direction and slightly decrease the power supplied to the + motors on the other side. So moving the drone forward is achieved by increasing the power to the `BOTTOM_LEFT` + and `BOTTOM_RIGHT` motors and decreasing the power to the `TOP_LEFT` and `TOP_RIGHT` motors, and the other way around + if you want to move the drone backwards. + +4. _Yaw_: you can use this movement to rotate the drone around the vertical axis without changing its inclination. This + is particularly used when you want to take 360 degrees pictures or videos of the area around the drone. This movement + is achieved by increasing the power supplied to the motors that rotate in the opposite direction as the desired + direction, and decreasing the power supplied to the other two motors. So rotating the drone clockwise is achieved by + increasing the power to the `TOP_RIGHT` and `BOTTOM_LEFT` motors and decreasing the power to the `TOP_LEFT` and + `BOTTOM_RIGHT` motors, and the other way around if you want to rotate the drone anti-clockwise. + +## Controlling the movement through the joystick + +Now that we have an understanding of how to get the drone to move in any direction, it's time to translate our +understanding into code by mapping these movements to joystick controls. Most of the controllers available for the +commercial drones use this configuration: + +![Common mapping between joystick controls and drone movements (credits: https://smaccmpilot.org/hardware/rc-controller.html)](../img/pitch-roll-yaw-2.png) + +I'll assume that your joystick has analog axis controllers, since it makes things more consistent with the common +conventions use by the drones on the market. + +First, let's write a `.move()` method to move the drone in each of the desired directions. For simplicity, we'll assume +that the drone can do one movement at the time (e.g. it can't climb and yaw, or roll and pitch, at the same time), but +the code can easily be extended with more complex logic: + +```python +from enum import IntEnum +from logging import getLogger +from threading import RLock + +from platypush.context import get_plugin + +class Movement(IntEnum): + THROTTLE = 0 + PITCH = 1 + ROLL = 2 + YAW = 3 + +class Channel: + TOP_LEFT = 0 + TOP_RIGHT = 1 + BOTTOM_LEFT = 2 + BOTTOM_RIGHT = 3 + +static_thrusts = { + Channel.TOP_LEFT: ..., + Channel.TOP_RIGHT: ..., + Channel.BOTTOM_LEFT: ..., + Channel.BOTTOM_RIGHT: ..., +} + +movement_lock = RLock() +logger = getLogger(__name__) +# Base step offset for "smoothening" the PWM changes +step = 25 +# Duration of each step pulse during the transients +step_duration = 0.03 + +def normalize_thrust(channel: int, percent: float) -> int: + """ + Converts a percent change to one of the channels into a + duty cycle integer that can be sent to the PWM controller. + The base value for all the percentage changes is always the + static thrust value for a given channel. + """ + pwm = get_plugin('pwm.pca9685') + static_thrust = static_thrusts[channel] + percent = max(-1., min(1., percent)) + thrust = static_thrust * (1 + percent) + return int( + max(pwm.min_duty_cycle, min(thrust, pwm.max_duty_cycle)) + ) + + +def move(movement: Movement, percent: float): + """ + :param movement: Direction for the movement. + :param percent: Percentage of change compared to the + static_thrusts configuration, must be between -1.0 and 1.0. + """ + channel_values = static_thrusts.copy() + + if movement == Movement.THROTTLE: + channel_values = { + channel: normalize_thrust(channel, percent=percent) + for channel in channel_values.keys() + } + elif movement == Movement.ROLL: + channel_values = { + channel: normalize_thrust( + channel, percent=percent/2 + if channel in [Channel.TOP_LEFT, Channel.BOTTOM_LEFT] + else -percent/2 + ) + for channel in channel_values.keys() + } + elif movement == Movement.PITCH: + channel_values = { + channel: normalize_thrust( + channel, percent=percent/2 + if channel in [Channel.BOTTOM_LEFT, Channel.BOTTOM_RIGHT] + else -percent/2 + ) + for channel in channel_values.keys() + } + elif movement == Movement.YAW: + channel_values = { + channel: normalize_thrust( + channel, percent=percent/2 + if channel in [Channel.TOP_RIGHT, Channel.BOTTOM_LEFT] + else -percent/2 + ) + for channel in channel_values.keys() + } + + # Ensure that only one thread at the time can control + # the current motion of the drone + with movement_lock: + logger.info(f'Moving drone: movement={movement.name}, percent={percent}') + pwm = get_plugin('pwm.pca9685') + pwm.write(channels=channel_values, step=step, step_duration=step_duration) +``` + +Now that we have a function that can easily translate the desired percentage of change along a certain direction into +the appropriate PWM values for the motor channels, let's connect them to our joystick controls. With Platypush running +and the controller connected, move the analog controls on your joystick and check the lines logged by the application. +You should see events like this: + +``` +Received event: { + "type": "event", + "args": { + "type": "platypush.message.event.joystick.JoystickAxisEvent", + "device": "/dev/input/js0", + "axis": "x", + "value": 0.5 + } +} +``` + +A joystick's analog controls are usually mapped to four axes: the left control's horizontal and vertical axes are +respectively mapped to _x_ and _y_, and the right control's horizontal and vertical axes are respectively mapped to +_z_ and _rz_. From the previous drone controller schema, we know that the _x_ axis should be associated to the yaw +movement, the _y_ axis is associated to the throttle (up/down) movement, the _z_ axis is associated to the roll movement +and the _rz_ axis is associated to the pitch movement. Platypush already outputs events with values between -1 and 1, +where 0 represents the middle point (analog control in "rest" position), -1 represents the control being at the leftmost +position (or bottom if it's a vertical axis) and 1 represents the control being at the rightmost position (or top if +it's a vertical axis). With these considerations in mind, we can easily write an event hook that translates a joystick +event into a drone action: + +```python +from platypush.event.hook import hook +from platypush.message.event.joystick import JoystickAxisEvent + +@hook(JoystickAxisEvent) +def on_joystick_axis_event(event: JoystickAxisEvent, **_): + movement = None + if event.axis == 'x': + movement = Movement.YAW + elif event.axis == 'y': + movement = Movement.THROTTLE + elif event.axis == 'z': + movement = Movement.ROLL + elif event.axis == 'rz': + movement = Movement.PITCH + + if movement: + move(movement, percent=event.value) +``` + +Now restart Platypush, make sure that the controller is paired, lift off and start moving your drone around. If you have +made it so far, congratulations on building and flying drone from scratch! + +# Conclusions + +By the end of this article you have learned how to design, build and fly your drone from scratch, but many improvements +can be applied to the base model we've put together here. Some ideas: + +- Integrate an accelerometer to detect whether the drone "leans" too much in a direction other than the expected one, + and automatically adjust the thrusts to keep the drone stable. Platypush provides built-in support for some types of + accelerometers connected over I2C or SPI. +- Move from a Bluetooth or IR joystick to an RC (radio-controlled) one, so you can control your drone over longer + ranges. If the RC controller is mapped as a standard joystick on Linux then all the code we've written for the + controller shouldn't require any changes. +- Add a GPS module (Platypush provides support for any GPS device supported by `gpsd`) and a 3G/4G module to get the + location of the drone and communicate with it even when it's not connected to your Wi-Fi network. +- Make a command-line script or a small UI application to control the drone from your computer or phone. The application + can still call the `.move()` method we have defined previously to control the vehicle. + +Good hacking!