<?xml version="1.0" encoding="utf-8"?><feed xmlns="http://www.w3.org/2005/Atom" ><generator uri="https://jekyllrb.com/" version="3.10.0">Jekyll</generator><link href="/feed.xml" rel="self" type="application/atom+xml" /><link href="/" rel="alternate" type="text/html" /><updated>2026-09-08T20:17:42+00:00</updated><id>/feed.xml</id><title type="html">Duct Tape, Bubble Gum, and Solder</title><subtitle>A personal website that collects my projects, musings, and miscellany side projects into a semi-coherent collection.</subtitle><entry><title type="html">Analog Noise Machines - Simple Oscillators and the Atari Punk Console</title><link href="/blog/2026/9/8/analog-noise-machines" rel="alternate" type="text/html" title="Analog Noise Machines - Simple Oscillators and the Atari Punk Console" /><published>2026-09-08T00:00:00+00:00</published><updated>2026-09-08T00:00:00+00:00</updated><id>/blog/2026/9/8/analog-noise-machines</id><content type="html" xml:base="/blog/2026/9/8/analog-noise-machines"><![CDATA[<ul id="markdown-toc">
  <li><a href="#overview" id="markdown-toc-overview">Overview</a></li>
  <li><a href="#the-most-basic---super-simple-oscillator" id="markdown-toc-the-most-basic---super-simple-oscillator">The most basic - Super Simple Oscillator</a>    <ul>
      <li><a href="#how-it-works" id="markdown-toc-how-it-works">How it works</a></li>
    </ul>
  </li>
  <li><a href="#next-step-up---atari-punk-console" id="markdown-toc-next-step-up---atari-punk-console">Next Step Up - Atari Punk Console</a>    <ul>
      <li><a href="#how-it-works-1" id="markdown-toc-how-it-works-1">How it works</a></li>
      <li><a href="#my-build" id="markdown-toc-my-build">My Build</a></li>
      <li><a href="#modifications" id="markdown-toc-modifications">Modifications</a>        <ul>
          <li><a href="#switching-headphone-jack" id="markdown-toc-switching-headphone-jack">Switching Headphone Jack</a></li>
          <li><a href="#cv-control" id="markdown-toc-cv-control">CV Control</a></li>
        </ul>
      </li>
      <li><a href="#final-results" id="markdown-toc-final-results">Final Results</a></li>
      <li><a href="#next-steps" id="markdown-toc-next-steps">Next Steps</a></li>
    </ul>
  </li>
  <li><a href="#resources" id="markdown-toc-resources">Resources</a></li>
</ul>

<p>You ever feel like you just want to make some noise? Maybe a horrendous noise? And you want to use electronics to do it? Well do I have some fun projects for you, lets dive into the world of simple analog noise makers!</p>

<p><img src="/assets/atari-punk-console/apc_in_box.jpg" alt="Picture of a black box on a blue anti-static mat. The box has a toggle switch and LED in the upper left corner, two knobs, one in each bottom left corner, and the words &quot;Atari Punk Console&quot; written in sharpie on pink duct tape taped to the top and side of the box" /></p>

<!--more-->

<h1 id="overview">Overview</h1>

<p>Nowdays most intro-to-sound tutorials will have you grab an arduino or other microcontroller and a speaker, but <em>back in the day</em>, things were still analog. No computer, just a bunch of electrical components to use (or abuse) to create oscillating voltages with different frequencies to make sound. This eventually branched out into synthesizers and electronic music we’ve all come to know and love. But you don’t <em>have</em> to drop <a href="https://www.perfectcircuit.com/korg-monologue.html">$450 on an analog synthesizer</a> in order to make some loud, synth-like sounds.</p>

<h1 id="the-most-basic---super-simple-oscillator">The most basic - Super Simple Oscillator</h1>

<p>Probably the simplest I’ve found is this super simple oscillator from <a href="https://www.lookmumnocomputer.com/simplest-oscillator/">Look Mum No Computer</a>. This is created by abusing a poor transistor that doesn’t know whats coming for em.</p>

<p><img src="https://images.squarespace-cdn.com/content/v1/5912db7ee6f2e1435dbab4fd/1508162688304-7GZ5DO0HRRZNXO52211Y/SUPER+SIMPLE+OSCILLATOR+SCHEMATIC?format=2500w" alt="Drawing of a circuit schematic, maybe done with MS paint. From top to bottom it has positive 9V, then a resistor and potentiometer in series, then a capacitor and transistor in parallel, then an LED and ground." /></p>

<h2 id="how-it-works">How it works</h2>

<p>The basic concept has you hook up a transistor’s collector and emittor <strong>backwards</strong> in the circuit, in parallel with a capacitor, with a resistor in series. And just to be even meaner to the transistor, you cut off the base middle pin (this helps prevent it from interfering with your shenanigans). The battery charges up the capacitor while the transistor basically has nothing flowing through it because it’s backwards. Once the capacitor reaches the right level of voltage, though, it triggers an “avalanche” in the transistor, where it suddenly has very low resistance. The capacitor discharges through the transistor until its voltage is lower than that trigger point, and the circuit goes back to its original state, with the capacitor charging up. This repeats, and you get some oscillation, and therefore, noise! If instead of a single resistor, you do a resistor plus potentiometer, you get the ability to control how fast the capacitor charges, and therefore the frequency of the oscillation.</p>

<p>The fun part of this circuit is:</p>

<ul>
  <li>if you have any sort of component collection, you probably already own all the parts for it (I did!)</li>
  <li>exactly which value of each component is super loose. You can use different NPN transistors, which will trigger at different voltages. You can use different sized capacitors, which determine the baseline frequency your pot is changing. You can add an LED, or not.</li>
</ul>

<p>After playing around with it, it kind of felt like you could just throw a handful of random components on it, and it’d somehow still oscillate. Fun stuff! I messed around with this circuit a lot on my breadboard, and did some double oscillator for two tones sort of thing as well:</p>

<p><img src="/assets/atari-punk-console/simple_oscillator_breadboard.jpg" alt="photo of a messy black desk with a breadboard and wires. You can tell the wires are connected to speakers with alligator clips, and there appear to be multiple capacitors, transistors, and resistors on the breadboard, but the exact layout is unclear." /></p>

<p>However, there’s definite limitations to what sort of sounds you can get out of it. So let’s go a little bigger.</p>

<h1 id="next-step-up---atari-punk-console">Next Step Up - Atari Punk Console</h1>

<p>While it’s not individual components, a classic 555 timer chip counts as analog to me. So how about two of them? Lets build the Atari Punk Console.</p>

<blockquote>
  <p><strong>NOTE</strong> There’s a <em>lot</em> of different circuits and walk throughs online. If you want to see how variable they can be, it’s fun to compare the circuits with different values and compare/contrast. Well, fun to me, anyway.</p>
</blockquote>

<h2 id="how-it-works-1">How it works</h2>

<p>I ended up using the circuit from this website: 
<a href="https://www.build-electronic-circuits.com/atari-punk-console/">https://www.build-electronic-circuits.com/atari-punk-console/</a></p>

<p>Mainly because it included some resistors that other versions lack. For example without, look at <a href="https://electronics.stackexchange.com/questions/102592/problems-with-atari-punk-console-circuit">the circuit this user built on electronics stack exchange</a>. This version of the Atari Punk Console has the second potentiometer conect the power source to pin 7 on the second 555 with no resistor in series. So if the potentiometer is turned all the way down, the resistance is 0 and the pin is getting full power. So when the 555’s internal transistor discharges through the chip, it’s just shorting out. Schematic shown below.</p>

<blockquote>
  <p><strong>Fun Fact</strong> Creating shorts across your battery is not good!</p>
</blockquote>

<p><img src="https://i.sstatic.net/Ou0kx.jpg" alt="" /></p>

<p>The linked stack exchange talks about what happens then, the timer gets very hot or even melts. Not so good for music making.</p>

<p>Throwing in a few resistors so there’s always <em>some</em> resistance on the connections between power and the potentiometer seemed like a good thing, so I went with that version. I also wanted a way to turn it off when I was done, though, so I added a switch and LED in-line with the power. The base schematic I ended up using:</p>

<p><img src="/assets/atari-punk-console/apc_schematic_base_version.png" alt="" /></p>

<h2 id="my-build">My Build</h2>

<p>After spending a concerningly long time at our local surplus store with a huge electronic component selection (<a href="https://www.ax-man.com/">Ax-man surplus</a>, how I love you), I had everything I needed.</p>

<p>I translated the schematic to a breadboard for initial testing, and it came together suprisingly easily.</p>

<p><img src="/assets/atari-punk-console/apc_breadboard.jpg" alt="photo of a breadboard with two IC chips, wires, two potentiometers, and a few resistors and capacitors. " /></p>

<p>Once I had proof of concept, I converted it over to a perfboard with soldered connections for a more permanent setup. Below are my in-progress photos as I went.</p>

<p>Getting started:
<img src="/assets/atari-punk-console/apc_perf_starting.jpg" alt="photo of a very cluttered black desk, a blue anti-static mat is in front with a helping hands set for soldering. Wires, hand tools, soldering tools are scattered across the desk. A small monitor can be seend hanging on the wall behind the desk, with a schematic on it." /></p>

<p>Partially finished with the perfboard layout, and a lot of messy long wires:
<img src="/assets/atari-punk-console/apc_perf_in_progress.jpg" alt="photo of a perfboard with some wires and components on it, including 2 ICs and some resistors. several long black and red wires curl around the board" /></p>

<p>All the components soldered together and working! Now I just need an enclosure…
<img src="/assets/atari-punk-console/apc_perf_finished_outside_box.jpg" alt="photo of a perfboard on a blue anti-static mat on a black desk. A quarter of the perfboard is full of wires and ICs and resistors. Wires connect the perfboard to a 9V battery holder, a toggle switch, a red LED, 2 poteniometers, and a speaker" /></p>

<p>I had a hackerbox subscription a while back, and their kits come in fun skull-and-circuit decorated boxes. Which seemed perfect for something called a <em>punk</em> console:
<img src="/assets/atari-punk-console/apc_in_box.jpg" alt="Picture of a black box on a blue anti-static mat. The box has a toggle switch and LED in the upper left corner, two knobs, one in each bottom left corner, and the words &quot;Atari Punk Console&quot; written in sharpie on pink duct tape taped to the top and side of the box" /></p>

<p>And just like that, the first version of the Atari Punk Console was done! But, well, we can do better, now can’t we…</p>

<h2 id="modifications">Modifications</h2>

<h3 id="switching-headphone-jack">Switching Headphone Jack</h3>

<p>My partner occasionally works from home. It turns out listening to loud annoying sounds repeatedly gets annoying. So I modified the original schematic to include a switching audio jack, courtesy of ax-man surplus. The jack is a standard headphone jack, and it has 3 connection points. When there’s no headphones plugged in, electricity will go through one path, and when headphones are plugged in, it goes through a different path. So if it’s spliced into the path to the speaker, I can set it up so when I plug in headphones, it goes through my headphones, and when I unplug them, it goes back out through the speakers.</p>

<p>I can now listen to my atari punk console quietly! Schematic change:</p>

<p><img src="/assets/atari-punk-console/apc_schematic_switching_jack.png" alt="Schematic. A switching headphone jack has been inserted in the schematic between the 555 OUT pin and the speaker" /></p>

<h3 id="cv-control">CV Control</h3>

<p>The 555 chip has a CV pin right on it, pin 5, which isn’t used at all for most atari punk consoles I’ve seen. However, it is possible to add that functionality pretty easily. This allows you to add some audio jack plugs that, when connected to an appropriate control voltage source (e.g. certain sythensizers), can control the exact tone you get. The addition is shown below.</p>

<p><img src="/assets/atari-punk-console/apc_schematic_CV_input.png" alt="Schematic. Two Vactrol ICs have been added, connecting the 555 CV pins to vactrols and resistors. The other side of the vactrols are connected to headphone jacks" /></p>

<p>As you can see, it’s adding a vactrol connected to an audio jack on one side, and the CV voltage pin on the other. The vactrol is just a photoresistor and an LED, so as the voltage goes up, the LED gets brighter, and the photoresistor changes resistance. The end result becomes: “Voltage change from the CV input results in a voltage change on the CV pin”. But with bonus that it keeps the atari punk console isolated from whatever is plugged into the CV input jack, since only light is being sent between the CV jack and the atari punk console.</p>

<p>Now, I didn’t actually buy a fancy vactrol chip or anything, and instead did the DIY version. This can be made by using a photoresistor and a LED, with some heat shrink to hold them together and keep out any light. And tada, vactrol!</p>

<blockquote>
  <p><strong>NOTE</strong> If you learn better via video, you can see Look Mum No Computer make one here: <a href="https://youtu.be/Ch9w5JtbZSc?si=L04PQ_3qpAdYQGnf&amp;t=215">https://youtu.be/Ch9w5JtbZSc?si=L04PQ_3qpAdYQGnf&amp;t=215</a></p>
</blockquote>

<h2 id="final-results">Final Results</h2>

<p>With some small mods to the enclosure, my new and improved atari punk console was complete! The two CV input jacks I put on the left hand side, with the headphone out jack in the lower right with a little label, so I didn’t forget which was which. I also added some cuts with an xacto knife over where the speaker was mounted in the upper right, to stop the cardboard from muffling the sound. And since I just free-handed it, that certainly gives it a, shall we say, <em>punk</em> aesthetic.</p>

<p><img src="/assets/atari-punk-console/apc_finished.jpg" alt="Picture of a black box on a blue anti-static mat. The box has a toggle switch and LED in the upper left corner, two knobs, one in each bottom left corner, and the words &quot;Atari Punk Console&quot; written in sharpie on pink duct tape taped to the top and side of the box. There are 3 headphone jacks sticking out the top of the box. One has the word &quot;out&quot; next to it in sharpie on pink duct tape" /></p>

<p>And if you want to hear what noises it can play, here’s a short recording. I’m simply turning the potentiometers in various ways to get the different tones.</p>

<audio controls="">
  <source src="/assets/atari-punk-console/atari_punk_console_basic_sound.mp3" type="audio/mpeg" />
  Your browser does not support the audio element.
</audio>

<h2 id="next-steps">Next Steps</h2>

<p>I started to play with the CV input, enough to prove it <em>can</em> control the 555 timers. However, I got distracted and decided to try building a 8-step sequencer to work with the atari punk console, and then started debating about adding a gate control so the sound on/off can be controlled by a separate syth of some type… So maybe that will be some future blog posts! In the meantime, I’ll just occasionally pull this out when I feel the need to make some good buzzy analog sounds.</p>

<h1 id="resources">Resources</h1>

<p>TI’s 555 timer documentation: <a href="https://www.ti.com/lit/ds/symlink/lm555.pdf">https://www.ti.com/lit/ds/symlink/lm555.pdf</a></p>

<p>Atari Punk Console Schematic <a href="https://www.build-electronic-circuits.com/atari-punk-console/">https://www.build-electronic-circuits.com/atari-punk-console/</a></p>

<p>Look Mum No Computer Super Simple Oscillator: <a href="https://www.lookmumnocomputer.com/simplest-oscillator/">https://www.lookmumnocomputer.com/simplest-oscillator/</a></p>

<p>Look Mum No Computer DIY Vactrol explanation: <a href="https://youtu.be/Ch9w5JtbZSc?si=L04PQ_3qpAdYQGnf&amp;t=215">https://youtu.be/Ch9w5JtbZSc?si=L04PQ_3qpAdYQGnf&amp;t=215</a></p>]]></content><author><name></name></author><category term="technical write up" /><category term="side project" /><category term="How-to" /><category term="side project" /><category term="embedded" /><category term="synthesizers" /><summary type="html"><![CDATA[You ever feel like you just want to make some noise? Maybe a horrendous noise? And you want to use electronics to do it? Well do I have some fun projects for you, lets dive into the world of simple analog noise makers!]]></summary></entry><entry><title type="html">Tracking Drones with Remote IDs - Panicking with the Colonel</title><link href="/blog/2026/8/21/drones_current_detection_sky_spy" rel="alternate" type="text/html" title="Tracking Drones with Remote IDs - Panicking with the Colonel" /><published>2026-08-21T00:00:00+00:00</published><updated>2026-08-21T00:00:00+00:00</updated><id>/blog/2026/8/21/drones_current_detection_sky_spy</id><content type="html" xml:base="/blog/2026/8/21/drones_current_detection_sky_spy"><![CDATA[<ul id="markdown-toc">
  <li><a href="#overview---functionality-and-code-review" id="markdown-toc-overview---functionality-and-code-review">Overview - Functionality and Code Review</a>    <ul>
      <li><a href="#ble-4-and-5" id="markdown-toc-ble-4-and-5">BLE 4 and 5</a></li>
      <li><a href="#5g-wifi" id="markdown-toc-5g-wifi">5G Wifi</a></li>
      <li><a href="#24g-wifi" id="markdown-toc-24g-wifi">2.4G Wifi</a></li>
      <li><a href="#parsing-remote-id-protocols" id="markdown-toc-parsing-remote-id-protocols">Parsing Remote ID protocols</a></li>
      <li><a href="#sky-spy-functionality-conclusion" id="markdown-toc-sky-spy-functionality-conclusion">Sky-Spy Functionality Conclusion</a></li>
    </ul>
  </li>
  <li><a href="#setup-with-diy-hardware" id="markdown-toc-setup-with-diy-hardware">Setup with DIY Hardware</a></li>
  <li><a href="#my-modified-codebase" id="markdown-toc-my-modified-codebase">My Modified Codebase</a>    <ul>
      <li><a href="#firmware-change-1---add-support-for-a-display" id="markdown-toc-firmware-change-1---add-support-for-a-display">Firmware Change 1 - Add support for a display</a></li>
      <li><a href="#firmware-change-2---add-support-for-buttons" id="markdown-toc-firmware-change-2---add-support-for-buttons">Firmware Change 2 - Add support for buttons</a></li>
      <li><a href="#hardware---adafruit-tft-reverse-feather" id="markdown-toc-hardware---adafruit-tft-reverse-feather">Hardware - Adafruit TFT reverse feather</a>        <ul>
          <li><a href="#teardown-conference" id="markdown-toc-teardown-conference">Teardown Conference</a></li>
        </ul>
      </li>
      <li><a href="#hardware---lilygo-t-dongle-s3" id="markdown-toc-hardware---lilygo-t-dongle-s3">Hardware - Lilygo T Dongle S3</a></li>
    </ul>
  </li>
  <li><a href="#current-detection-results" id="markdown-toc-current-detection-results">Current Detection Results</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<p>Since I <a href="/blog/2026/7/21/drone-detection">already talked</a> about the Remote ID protocol and how it was broadcast, you might now want to actually <em>listen</em> to those Remote ID messages around your home. There’s certainly commercial-grade solutions for that, but maybe you don’t want to drop a thousand dollars or book a meeting with a company so you can buy it, all for a fun sidequest. Instead, lets look for some slightly cheaper and easier to access options, and see what Remote ID functionality they support, and what they don’t.</p>

<p>First up: The first drone detection hardware/firmware I came across, Colonel Panic’s <a href="https://github.com/colonelpanichacks/Sky-Spy">Sky-Spy</a>! I’ll review the hardware and firmware functionality, how to DIY the hardware if you’re feeling cheap, and include a bonus of some forked code that I’ve started working on!</p>

<p>…I had originally planned on doing one overview post of all current available options…but then I got nerdsniped. So I’ll be going more in-depth on each.</p>

<p><img src="/assets/drones_current_detection/eagle.png" alt="Sky-Spy's logo, ASCII black and white art of a bird of prey attacking a quadcopter drone" /></p>

<!--more-->

<blockquote>
  <p><strong>NOTE</strong> Highly recommend reading my previous post about the remote ID transmission types, if you haven’t already. This post assumes you know about that already.</p>
</blockquote>

<h2 id="overview---functionality-and-code-review">Overview - Functionality and Code Review</h2>

<p><a href="https://colonelpanic.tech/">Colonel Panic</a> has several fun pieces of hardware, including their OUI-SPY, which was originally setup as a general BLE scanner, but was re-purposed by them as a drone detector as well. The custom hardware currently uses a Xiao ESP32-S3, a piezo buzzer, and custom PCBs to make a nice little handheld device. They used their hardware with a custom code base called <a href="https://github.com/colonelpanichacks/Sky-Spy">Sky-Spy</a> (The image above is the logo from that firmware). Colonel Panic has a lot of other cool firmware setup for the OUI-Spy as well, such as the <a href="github.com/colonelpanichacks/oui-spy-unified-blue">Unified Blue</a> firmware that combines a lot of different functionality into one codebase.</p>

<p>In the previous post, I talked about all the potential ways drones can broadcast: BLE 4 and 5 advertising packets, and 2.4/5G Wifi beacon and NAN frames. Based on my research, the OUI-Spy covers 3 out of possible 6 transmission types. How do I know? By looking at the current hardware and firmware. Lets take a look!</p>

<blockquote>
  <p><strong>NOTE</strong> They’re pretty active with development, so some of my research may be moot soon. <strong>BUT</strong> if you know how and where to look for this info now, you’ll be able to take a look at the latest code and compare!</p>
</blockquote>

<h3 id="ble-4-and-5">BLE 4 and 5</h3>

<p>For BLE, we first check if the hardware can handle 4 and 5, then look at the code. The hardware is easy, espressif specifically says ESP32S3’s can use both BLE 4 and 5.</p>

<p>For the firmware side, we can see it’s using pretty standard espressif setup for BLE advertisements. However, it’s not setup for the BLE 5 extended advertising packets, which can be checked by looking at the BLE 5 example code from espressif.</p>

<blockquote>
  <p><strong>Want to look at the code and follow along?</strong> The main Sky-spy code can be found here: <a href="https://github.com/colonelpanichacks/Sky-Spy">https://github.com/colonelpanichacks/Sky-Spy</a></p>
</blockquote>

<p>Here’s the callback in Sky-Spy:</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">MyAdvertisedDeviceCallbacks</span> <span class="o">:</span> <span class="k">public</span> <span class="n">BLEAdvertisedDeviceCallbacks</span> <span class="p">{</span>
<span class="nl">public:</span>
  <span class="kt">void</span> <span class="n">onResult</span><span class="p">(</span><span class="n">BLEAdvertisedDevice</span> <span class="n">device</span><span class="p">)</span> <span class="k">override</span> <span class="p">{</span>
    <span class="kt">int</span> <span class="n">len</span> <span class="o">=</span> <span class="n">device</span><span class="p">.</span><span class="n">getPayloadLength</span><span class="p">();</span>
    <span class="kt">uint8_t</span><span class="o">*</span> <span class="n">payload</span> <span class="o">=</span> <span class="n">device</span><span class="p">.</span><span class="n">getPayload</span><span class="p">();</span>
    <span class="c1">// payload parsing removed here for brevity...</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>And the BLE initialization in Sky-Spy:</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">BLEDevice</span><span class="o">::</span><span class="n">init</span><span class="p">(</span><span class="s">"DroneID"</span><span class="p">);</span>
  <span class="n">pBLEScan</span> <span class="o">=</span> <span class="n">BLEDevice</span><span class="o">::</span><span class="n">getScan</span><span class="p">();</span>
  <span class="n">pBLEScan</span><span class="o">-&gt;</span><span class="n">setAdvertisedDeviceCallbacks</span><span class="p">(</span><span class="k">new</span> <span class="nf">MyAdvertisedDeviceCallbacks</span><span class="p">());</span>
  <span class="n">pBLEScan</span><span class="o">-&gt;</span><span class="n">setActiveScan</span><span class="p">(</span><span class="nb">true</span><span class="p">);</span>
</code></pre></div></div>

<p>Lets compare that with the Espressif example code for extended advertising.</p>

<blockquote>
  <p><strong>NOTE</strong> The example code lives here:
<a href="https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/examples/BLE5_extended_scan/BLE5_extended_scan.ino">https://github.com/espressif/arduino-esp32/blob/master/libraries/BLE/examples/BLE5_extended_scan/BLE5_extended_scan.ino</a></p>
</blockquote>

<p>Here’s Espressif’s callback, note it’s inheriting from a differenc class, <code class="language-plaintext highlighter-rouge">BLEExtAdvertisingCallbacks</code>, and the <code class="language-plaintext highlighter-rouge">onResult</code> argument type is different:</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">class</span> <span class="nc">MyBLEExtAdvertisingCallbacks</span> <span class="o">:</span> <span class="k">public</span> <span class="n">BLEExtAdvertisingCallbacks</span> <span class="p">{</span>
  <span class="kt">void</span> <span class="n">onResult</span><span class="p">(</span><span class="n">esp_ble_gap_ext_adv_report_t</span> <span class="n">report</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">report</span><span class="p">.</span><span class="n">event_type</span> <span class="o">&amp;</span> <span class="n">ESP_BLE_GAP_SET_EXT_ADV_PROP_LEGACY</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// here we can receive regular advertising data from BLE4.x devices</span>
      <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"BLE4.2"</span><span class="p">);</span>
    <span class="p">}</span> <span class="k">else</span> <span class="p">{</span>
      <span class="c1">// here we will get extended advertising data that are advertised over data channel by BLE5 devices</span>
      <span class="n">Serial</span><span class="p">.</span><span class="n">printf</span><span class="p">(</span><span class="s">"Ext advertise: data_le: %u, data_status: %u </span><span class="se">\n</span><span class="s">"</span><span class="p">,</span> <span class="n">report</span><span class="p">.</span><span class="n">adv_data_len</span><span class="p">,</span> <span class="n">report</span><span class="p">.</span><span class="n">data_status</span><span class="p">);</span>
    <span class="p">}</span>
  <span class="p">}</span>
<span class="p">};</span>
</code></pre></div></div>

<p>And here’s the initialization for extended advertising, note it’s setting the extended scan functions:</p>
<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code> <span class="n">BLEDevice</span><span class="o">::</span><span class="n">init</span><span class="p">(</span><span class="s">""</span><span class="p">);</span>
  <span class="n">pBLEScan</span> <span class="o">=</span> <span class="n">BLEDevice</span><span class="o">::</span><span class="n">getScan</span><span class="p">();</span>  <span class="c1">//create new scan</span>
  <span class="n">pBLEScan</span><span class="o">-&gt;</span><span class="n">setExtendedScanCallback</span><span class="p">(</span><span class="k">new</span> <span class="nf">MyBLEExtAdvertisingCallbacks</span><span class="p">());</span>
  <span class="n">pBLEScan</span><span class="o">-&gt;</span><span class="n">setExtScanParams</span><span class="p">();</span>         <span class="c1">// use with pre-defined/default values, overloaded function allows to pass parameters</span>
  <span class="n">delay</span><span class="p">(</span><span class="mi">1000</span><span class="p">);</span>                          <span class="c1">// it is just for simplicity this example, to let ble stack to set extended scan params</span>
  <span class="n">pBLEScan</span><span class="o">-&gt;</span><span class="n">startExtScan</span><span class="p">(</span><span class="n">scanTime</span><span class="p">,</span> <span class="mi">3</span><span class="p">);</span>  <span class="c1">// scan duration in n * 10ms, period - repeat after n seconds (period &gt;= duration)</span>
</code></pre></div></div>

<p>So this research shows: <strong>Sky-Spy is capturing the BLE standard advertising packets, but not the extended ones</strong>.</p>

<p>However, the espressif example appears to get both legacy and extended advertising, which means this hardware could actually capture both. I plan on looking into this for my custom fork of this code (more below).</p>

<h3 id="5g-wifi">5G Wifi</h3>

<p>For determining if it supports 5G, we once again start with looking at the hardware. The ESP32-S3 hardware fact sheet shows it does not support 5G wifi. So this research shows: <strong>The ESP32-S3 only has 2.4 GHz Wi-Fi and BLE 5</strong>.</p>

<p>The new <a href="https://www.espressif.com/en/products/socs/esp32-c5">ESP32-C5</a> <em>does</em> support 5GHz wifi, and according to Espressif is the “first RISC-V MCU that supports 2.4 and 5 GHz dual-band Wi-Fi 6, along with Bluetooth 5 (LE) and IEEE 802.15.4 (Zigbee, Thread) connectivity”. So this may be a hardware upgrade for the future. And in fact Colonel panic’s repo has a sub-folder that looks to include some work done on the XIAO seeed ESP32C5 <a href="https://github.com/colonelpanichacks/Sky-Spy/tree/main/xiao-c5-5g">https://github.com/colonelpanichacks/Sky-Spy/tree/main/xiao-c5-5g</a>. So maybe this is a future upgrade for OUI-Spy?</p>

<h3 id="24g-wifi">2.4G Wifi</h3>

<p>We’ve already confirmed the ESP32S3 has 2.4 GHz Wi-Fi, so lets jump right to the firmware. Here we have to confirm both NAN and beacon frames are parsed.</p>

<p>First, the initialization, setting up the wifi module to be in promiscuous mode and scanning Channel 6:</p>

<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code>  <span class="n">WiFi</span><span class="p">.</span><span class="n">mode</span><span class="p">(</span><span class="n">WIFI_STA</span><span class="p">);</span>
  <span class="n">WiFi</span><span class="p">.</span><span class="n">disconnect</span><span class="p">();</span>
  
  <span class="n">esp_wifi_set_promiscuous</span><span class="p">(</span><span class="nb">true</span><span class="p">);</span>
  <span class="n">esp_wifi_set_promiscuous_rx_cb</span><span class="p">(</span><span class="o">&amp;</span><span class="n">callback</span><span class="p">);</span>
  <span class="n">esp_wifi_set_channel</span><span class="p">(</span><span class="mi">6</span><span class="p">,</span> <span class="n">WIFI_SECOND_CHAN_NONE</span><span class="p">);</span>
</code></pre></div></div>

<p>Looking at the <code class="language-plaintext highlighter-rouge">callback</code> function it references, it checks the packets to see if they’re NAN or beacon and parses them as needed (snippet below). The first <code class="language-plaintext highlighter-rouge">if</code> statement short circuits if the wifi packet isn’t a management packet. It then checks if the destination address is the expected NAN address, or if the management frame’s type is <code class="language-plaintext highlighter-rouge">0x80</code>, the beacon type.</p>

<div class="language-c++ highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kt">void</span> <span class="nf">callback</span><span class="p">(</span><span class="kt">void</span> <span class="o">*</span><span class="n">buffer</span><span class="p">,</span> <span class="n">wifi_promiscuous_pkt_type_t</span> <span class="n">type</span><span class="p">)</span> <span class="p">{</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">type</span> <span class="o">!=</span> <span class="n">WIFI_PKT_MGMT</span><span class="p">)</span> <span class="k">return</span><span class="p">;</span>
  
  <span class="n">wifi_promiscuous_pkt_t</span> <span class="o">*</span><span class="n">packet</span> <span class="o">=</span> <span class="p">(</span><span class="n">wifi_promiscuous_pkt_t</span> <span class="o">*</span><span class="p">)</span><span class="n">buffer</span><span class="p">;</span>
  <span class="kt">uint8_t</span> <span class="o">*</span><span class="n">payload</span> <span class="o">=</span> <span class="n">packet</span><span class="o">-&gt;</span><span class="n">payload</span><span class="p">;</span>
  <span class="kt">int</span> <span class="n">length</span> <span class="o">=</span> <span class="n">packet</span><span class="o">-&gt;</span><span class="n">rx_ctrl</span><span class="p">.</span><span class="n">sig_len</span><span class="p">;</span>
  
  <span class="k">static</span> <span class="k">const</span> <span class="kt">uint8_t</span> <span class="n">nan_dest</span><span class="p">[</span><span class="mi">6</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span><span class="mh">0x51</span><span class="p">,</span> <span class="mh">0x6f</span><span class="p">,</span> <span class="mh">0x9a</span><span class="p">,</span> <span class="mh">0x01</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">};</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">memcmp</span><span class="p">(</span><span class="n">nan_dest</span><span class="p">,</span> <span class="o">&amp;</span><span class="n">payload</span><span class="p">[</span><span class="mi">4</span><span class="p">],</span> <span class="mi">6</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">odid_wifi_receive_message_pack_nan_action_frame</span><span class="p">(</span><span class="o">&amp;</span><span class="n">UAS_data</span><span class="p">,</span> <span class="nb">nullptr</span><span class="p">,</span> <span class="n">payload</span><span class="p">,</span> <span class="n">length</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">)</span> <span class="p">{</span>
      <span class="c1">// parsing removed for brevity...</span>
    <span class="p">}</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">payload</span><span class="p">[</span><span class="mi">0</span><span class="p">]</span> <span class="o">==</span> <span class="mh">0x80</span><span class="p">)</span> <span class="p">{</span>
   <span class="c1">// parsing removed for brevity...</span>
  <span class="p">}</span>
<span class="p">}</span>
</code></pre></div></div>

<p>Using those two things as the way to check for NAN or Beacon frames is a bit odd, but does show <strong>Sky-Spy parses Wifi NAN and Beacon Frames in 2.4G Wifi</strong>.</p>

<h3 id="parsing-remote-id-protocols">Parsing Remote ID protocols</h3>

<p>Something I like about this codebase is that it uses the open remote ID library: <a href="https://github.com/opendroneid/opendroneid-core-c">https://github.com/opendroneid/opendroneid-core-c</a> for parsing the received messages. This parsing library was originally put together by Gabriel Cox, who was “leading the ASTM UAS Remote ID workgroup (as Chairman) consisting of industry and government stakeholders to bring a consensus standard solution and Means of Compliance (MOC) for Remote ID.” (quote pulled from <a href="https://coxdatalabs.com/about/">his company website</a>). It also claims in the README to be compliant with the updated F3411-22a, the version of the specification that currently does not appear to be available online unless you pay ASTM for the pleasure.</p>

<p>I wanted to call this out specifically, because some other solutions look to use custom parsing instead of a library, which has its own trade-offs.</p>

<h3 id="sky-spy-functionality-conclusion">Sky-Spy Functionality Conclusion</h3>

<p>You can see above then that the Sky-Spy firmware and the OUI-Spy can detect a subset of ways the drones transmit remote IDs: BLE 4, and 2.4G Wifi Beacon and NAN frames. This means, for example, DJI drones that broadcast on 2.4G Wifi will be heard, but Skydio drones that use 5G-only will not. With the custom hardware currently $85 on Colonel Panic’s website, that may be too steep a price for you, for the functionality given. If you’re feeling handy (which you may be, since you’re reading this), I do want to mention Colonel Panic does give enough info you can do a DIY OUI-Spy device, which may make the trade-off worth it for you.</p>

<h2 id="setup-with-diy-hardware">Setup with DIY Hardware</h2>

<p>When I was first interested in this, the <a href="https://colonelpanic.tech/">official OUI-Spy Hardware</a> was estimated a month or so out. However, Digikey sells the parts that Colonel Panic mentions as a DIY option, and has a much faster turnaround. So with a little bit of perf board and soldering, I had a functionally-identical bit of hardware that cost roughly 10 dollars and arrived faster. Downside, of course, is that I had to do a lil bit of soldering and firmware flashing. Luckily, that’s my jam.</p>

<p>I picked up a 3-pack of the ESP32S3’s, since it was a bit cheaper per-board: <a href="https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010573/24814471">https://www.digikey.com/en/products/detail/seeed-technology-co-ltd/102010573/24814471</a></p>

<p>And piezo buzzers are pretty interchangeable, but I grabbed these: <a href="https://www.digikey.com/en/products/detail/tdk/PS1720P02/935932">https://www.digikey.com/en/products/detail/tdk/PS1720P02/935932</a></p>

<p>Then you just connect the buzzer to the ESP32-S3 on the right pins (ground and the pin the firmware uses). To keep it all together, I used a small chunk of <a href="https://www.adafruit.com/product/2670">bakelite perf board</a> I had bought ages ago from adafruit, and some solid core wire. I routed the wire under the ESP32-S3, so it was as small of footprint as I could make it.</p>

<table>
  <thead>
    <tr>
      <th>ESP32-S3</th>
      <th>Connection</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>GPIO Pin 3</td>
      <td>Piezo Buzzer</td>
    </tr>
    <tr>
      <td>Ground</td>
      <td>Piezo Buzzer</td>
    </tr>
  </tbody>
</table>

<p>With the hardware put together, I just had to flash the firmware onto it. I had to make sure VSCode was installed, and the platform IO extension was loaded in VSCode. Then I cloned the Sky-Spy repo, and did a basic platform IO build and upload, while my device was plugged in with a data USB-C cable. And voila, a functioning drone detector!</p>

<p>Below are some pictures of my final product!</p>

<blockquote>
  <p><strong>Note</strong> I’ve added a larger antenna as my external antenna for the Wifi, since I had one lying around. You can just use the small antenna that comes with the Xiao ESP32-S3, you’ll probably just get a smaller range with it.</p>
</blockquote>

<p><img src="/assets/drones_current_detection/colonel_panic_xiao_esp32s3.jpg" alt="Picture of a large omni-directional antenna connected with a black wire to a small development board, next to a piezo buzzer, on a blue background. You can just barely see that the development board and buzzer are on a brown perf board that has clearly been cut to a small size." /></p>

<p>And the back side to see my “better than good, it’s good enough” soldering on the bit of perf board I used:
<img src="/assets/drones_current_detection/colonel_panic_xiao_esp32s3_back.jpg" alt="Picture of the backside that is mostly the brown perf board, with edges that are roughly cut to a small size. 2 pins of the development board have been soldered to a hidden wire, which then comes out lower down on the board and connects to pins on the piezo buzzer. The soldering looks slightly blobby but functional." /></p>

<h2 id="my-modified-codebase">My Modified Codebase</h2>

<p>I can never leave well enough alone, so I forked Colonel Panic’s codebase and started adding support for other hardware types. Codebase is <a href="https://github.com/Dthurow/Sky-Spy">here</a>. So far I’ve added and tested the Adafruit TFT reverse feather and a Lilygo T Dongle S3.</p>

<p>I also plan to upgrade the codebase to support both legacy and extended advertising BLE packets. As well as take some of the code changes from the <a href="https://github.com/colonelpanichacks/flock-you">Flock-you</a> codebase that Colonel Panic also made. Mainly writing to memory for detection retention through power-loss.</p>

<h3 id="firmware-change-1---add-support-for-a-display">Firmware Change 1 - Add support for a display</h3>
<p>I’ve updated the code to use a <code class="language-plaintext highlighter-rouge">DisplayHandler</code> interface that gets implemented by the specific bits of hardware. This then lets the <code class="language-plaintext highlighter-rouge">main.cpp</code> pull in the correct implementation at the top, and then just use the interface functions to interact with it in the code. This cuts down on the number of <code class="language-plaintext highlighter-rouge">#ifdef</code>s in the code and makes it easy to add more hardware going forward. So for example, my Adafruit board creates a class <code class="language-plaintext highlighter-rouge">Display_TFT_Reverse_Feather</code> to implement the <code class="language-plaintext highlighter-rouge">DisplayHandler</code>.</p>

<p>The interface has a couple standard functions:</p>
<ul>
  <li>Idle function, when I simply want to say it’s scanning, plus total detections and how many are in range currently</li>
  <li>Detection function, when it shows drone information (MAC, RSSI, GPS)</li>
  <li>Info function, for just general info updates that aren’t the above two options</li>
  <li>Initialization function, for configuring the hardware for that particular board</li>
</ul>

<p>For the implemntation on the feather, I used Adafruit’s example code a lot, using their custom TFT library, and it was pretty straightforward.</p>

<p>For the implementation on the lilygo, I used the <a href="github.com/Bodmer/TFT_eSPI">Bodmer TFT_eSPI</a> library, which was a bit of a bear to get working correctly. Selecting the configuration for the library felt confusing, and the current configuration for the lilygo t dongle doesn’t actually work with my hardware. I was using platform IO, which auto downloads the library for me, but then I had to modify my build flags so it used the correct configuration file (<code class="language-plaintext highlighter-rouge">TFT_eSPI/User_Setups/Setup209_LilyGo_T_Dongle_S3.h</code>), plus a bonus definition because my dongle apparently doesn’t match the standard setup (<code class="language-plaintext highlighter-rouge">-DUSE_HSPI_PORT</code>). Once I <em>finally</em> figured that out, the rest of the display implementation went quickly.</p>

<h3 id="firmware-change-2---add-support-for-buttons">Firmware Change 2 - Add support for buttons</h3>
<p>This currently assumes that if the buttons are enabled (via the <code class="language-plaintext highlighter-rouge">BUTTONS_ENABLED</code> definition), there will be 3 of them in a certain configuration (i.e. the reverse feather’s configuration). Eventually I’ll probably abstract things out similar to how the display is done above.</p>

<p>My code sets up interrupts on button presses, that feed to a queue that periodically gets serviced by the rtos task <code class="language-plaintext highlighter-rouge">interactionTask</code>. There’s a mini state machine there that sets up two buttons as a scroll up and scroll down button, with another one as a “detection dump” button. The scroll buttons lets you scroll between the currently in-range drones, looping back to the idle state when you finish.</p>

<p>The detection dump button triggers outputting all detections as json over serial. This supports using the reverse feather with a battery while you’re out and about, then when you get home, plug in the USB-C to your computer, open a serial connection, and press the button. Ta-da, all detections have been copied over! This isn’t maintained during power off events, but hey, it’s better than nothing.</p>

<h3 id="hardware---adafruit-tft-reverse-feather">Hardware - Adafruit TFT reverse feather</h3>

<p>This gives a display to play with, to show drone detections immediately, some buttons, and of course an external antenna option. If you wanted, you could keep the buzzer by buying one and wiring it up to a free GPIO, there’s definitely room, but I haven’t gotten around to that yet, I was too excited by the display.</p>

<p>Here’s a link to the hardware list on adafruit: <a href="https://www.adafruit.com/wishlists/622656">https://www.adafruit.com/wishlists/622656</a>. The total is about 48 dollars, but if you want to skip the battery, or buy the internal antenna option, you could cut the price down considerably. Though this comes with functionality trade offs, of course.</p>

<p>A nice benefit from using the Adafruit TFT reverse feather is that it comes with a battery charger chip onboard, and a JST connector for a lipoly battery. Which means it’s super easy to power with <em>either</em> an external USB-C connector, or with an internal battery, for portability. I’m currently using the very advanced prototyping functionality of “cardboard boxes” to create a custom enclosure for this hardware, but someone with 3d printing knowledge could probably knock out a slick case for it pretty fast.</p>

<p>Here’s some pictures of that current setup:</p>

<p><img src="/assets/drones_current_detection/adafruit_reverse_tft_top.jpg" alt="photograph of a small square cardboard box on a blue background. The box has a roughly rectangular hole cut into it, with the adafruit's display showing through the hole. The upper right has a large black omni antenna sticking out. " /></p>

<p><img src="/assets/drones_current_detection/adafruit_reverse_tft_inside.jpg" alt="photograph of the inside of a cardboard box. You can see the opposite side of the adafruit feather, and the antenna wire connecting from the feather to the antenna connector outside of the box. There is also a silver lipoly batter with a JST connector plugged into the feather" /></p>

<h4 id="teardown-conference">Teardown Conference</h4>
<p>I took this custom setup to <a href="https://www.crowdsupply.com/teardown/portland-2026">Teardown 2026</a> and just had it out on a table with little infographics for part of the conference. I’ve included the infographic images below if you’re curious!</p>

<p><img src="/assets/drones_current_detection/tracking_drones_with_remote_id_infographic.jpg" alt="photograph of a clearly handrawn page full of information covering basic transmission types of Remote ID, with simple drone drawings and arrows" /></p>

<p><img src="/assets/drones_current_detection/demo_info_infographic.jpg" alt="photograph of another handrawn page explaining the hardware, software, and use of the custom adafruit TFT feather setup." /></p>

<blockquote>
  <p><strong>NOTE</strong> Teardown is a super fun conference, highly recommended</p>
</blockquote>

<h3 id="hardware---lilygo-t-dongle-s3">Hardware - Lilygo T Dongle S3</h3>

<p>The <a href="https://www.microcenter.com/product/703093/lilygo-t-dongle-s3-esp32-s3-ttgo-development-board-with-screen">Lilygo T-Dongle-S3</a> is another setup that gives me a display, but the biggest plus is the cute thumbdrive form factor, and how it was in stock at my local Microcenter so I could impulse buy it. Current cost was about 20 bucks (looks like direct from lilygo you can get it for cheaper). It has a built in antenna, but interestingly, there’s an option to update it to use an external antenna. The iPex connector is on the board, just disconnected, so you have to remove a 0 ohm resistor from one place and bridge two solder pads in another place, then boom, external antenna support. The snap on plastic case also snaps off pretty easily, from my experience, so this seems like a pretty easy process. I haven’t done more than taken a look though, so there may be some hidden issues there.</p>

<p><img src="/assets/drones_current_detection/lilygo_t_dongle_s3_sky_spy.jpg" alt="Photo of a thumbdrive with a large screen and clear plastic case plugged into a laptop. The thumbdrive's screen has the word &quot;Scanning&quot; in large green letters on it." /></p>

<p>Another benefit of this is that it has a built in spot for a tiny SD card, funnily enough <strong>inside</strong> of the USB plug. This can be super useful for keeping data through a power off event, and is one of the reasons I want to look into saving detections to SD cards.</p>

<h2 id="current-detection-results">Current Detection Results</h2>

<p>A neighbor was kind enough to fly a drone around for me, to let me test out some ranges of different solutions. He had a DJI drone, and we tested it around my neighborhood, which is relatively flat, with 2-story buildings and some trees. This is important context since wifi and bluetooth are both affected by things in the way, so if you live someplace where there’s no trees and hills, you’ll get better range than, say, hilly and tree-covered seattle. Where and how big the antenna is will affect range too. If you or the drone are higher up, you’ll get a better line-of-sight (or line-of-signal, I suppose) and so better range.</p>

<p>With my smaller antenna, at ground level, with the drone flying between 100 and 300 feet above ground level, my sky-spy setup can detect drones about 500-800 feet away consistently.</p>

<p>This is one day of testing, so there’s definitely wriggle room in that number, but it feels very promising considering I hadn’t tested if it worked at all yet!</p>

<p>There’s other drone detection setups out there (though not that many), but this project has a relatively low cost of entry with decent range of detection. I feel either Colonel Panic’s or my codebase will continue to improve, too, making it a project to watch.</p>

<h2 id="references">References</h2>

<p>Colonel Panic: <a href="https://colonelpanic.tech/">https://colonelpanic.tech/</a></p>

<p>Sky-Spy: <a href="github.com/colonelpanichacks/Sky-Spy">github.com/colonelpanichacks/Sky-Spy</a></p>

<p>My Modified codebase: <a href="https://github.com/Dthurow/Sky-Spy">https://github.com/Dthurow/Sky-Spy</a></p>

<p>Adafruit TFT reverse feather drone kit: <a href="https://www.adafruit.com/wishlists/622656">https://www.adafruit.com/wishlists/622656</a></p>

<p>Lilygo T Dongle S-3: <a href="https://www.microcenter.com/product/703093/lilygo-t-dongle-s3-esp32-s3-ttgo-development-board-with-screen">https://www.microcenter.com/product/703093/lilygo-t-dongle-s3-esp32-s3-ttgo-development-board-with-screen</a></p>

<p>open remote ID library: <a href="https://github.com/opendroneid/opendroneid-core-c">https://github.com/opendroneid/opendroneid-core-c</a></p>]]></content><author><name></name></author><category term="technical write up" /><category term="side project" /><category term="How-to" /><category term="side project" /><category term="embedded" /><category term="remote ID" /><summary type="html"><![CDATA[Since I already talked about the Remote ID protocol and how it was broadcast, you might now want to actually listen to those Remote ID messages around your home. There’s certainly commercial-grade solutions for that, but maybe you don’t want to drop a thousand dollars or book a meeting with a company so you can buy it, all for a fun sidequest. Instead, lets look for some slightly cheaper and easier to access options, and see what Remote ID functionality they support, and what they don’t. First up: The first drone detection hardware/firmware I came across, Colonel Panic’s Sky-Spy! I’ll review the hardware and firmware functionality, how to DIY the hardware if you’re feeling cheap, and include a bonus of some forked code that I’ve started working on! …I had originally planned on doing one overview post of all current available options…but then I got nerdsniped. So I’ll be going more in-depth on each.]]></summary></entry><entry><title type="html">Tracking Drones with Remote IDs</title><link href="/blog/2026/7/21/drone-detection" rel="alternate" type="text/html" title="Tracking Drones with Remote IDs" /><published>2026-07-21T00:00:00+00:00</published><updated>2026-07-21T00:00:00+00:00</updated><id>/blog/2026/7/21/drone-detection</id><content type="html" xml:base="/blog/2026/7/21/drone-detection"><![CDATA[<ul id="markdown-toc">
  <li><a href="#remote-id-basics" id="markdown-toc-remote-id-basics">Remote ID Basics</a></li>
  <li><a href="#wifi" id="markdown-toc-wifi">Wifi</a>    <ul>
      <li><a href="#wifi-nan-discovery-frames" id="markdown-toc-wifi-nan-discovery-frames">Wifi NAN discovery frames</a></li>
      <li><a href="#beacon" id="markdown-toc-beacon">Beacon</a></li>
    </ul>
  </li>
  <li><a href="#bluetooth" id="markdown-toc-bluetooth">Bluetooth</a>    <ul>
      <li><a href="#bluetooth-4x" id="markdown-toc-bluetooth-4x">Bluetooth 4.x</a></li>
      <li><a href="#bluetooth-5" id="markdown-toc-bluetooth-5">Bluetooth 5</a></li>
    </ul>
  </li>
  <li><a href="#remote-id-messages" id="markdown-toc-remote-id-messages">Remote ID messages</a>    <ul>
      <li><a href="#message-pack" id="markdown-toc-message-pack">Message Pack</a></li>
      <li><a href="#basic-id" id="markdown-toc-basic-id">Basic ID</a></li>
      <li><a href="#locationvector" id="markdown-toc-locationvector">Location/Vector</a></li>
      <li><a href="#system" id="markdown-toc-system">System</a></li>
    </ul>
  </li>
  <li><a href="#using-this-info---wireshark" id="markdown-toc-using-this-info---wireshark">Using This Info - Wireshark</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
</ul>

<p>Drones have gotten more and more popular lately, from filming fun things on social media, <a href="https://www.aboutamazon.com/news/operations/mk30-drone-amazon-delivery-packages">delivering packages for Amazon</a>, being <a href="https://kstp.com/kstp-news/top-news/st-paul-police-new-drones-hit-scenes-in-under-90-seconds-surpass-last-years-drone-flights/">used by law enforcements in the states</a>, all the way to <a href="https://www.militarytimes.com/news/pentagon-congress/2026/03/11/these-are-ukraines-1000-interceptor-drones-the-pentagon-wants-to-buy/">being used in the ukraine war</a>. Understanding the technology and building tech to <em>track</em> the drones seems very useful. We’ve had public tracking of airplanes via ADSB for years at this point (see my previous post <a href="/blog/2026/3/3/adsb-radio-setup">about setting up your own flight tracker with ADSB</a>), but no equivalent with drones. Partially this is because drones, up until recently, didn’t really have ways <em>to</em> track them, unless you were visually tracking them or doing some advanced radio frequency shenanigans.</p>

<p>That has changed relatively recently (in 2023) when the FAA started requiring Remote IDs for drones, the functional equivalent to the broadcast ADSB info, but for drones specifically. Specific information about the drone, its location, and the drone operator are broadcast in one of the variety of ways specified by the standard put out by the ASTM (a standards governing body), which is charmingly named “F3411”. With this new ability, it’s possible to start tracking and aggregating drone flight information, same as ADSB. As the FAA website shows (screenshot below), there are now several ways to be compliant with the remote ID requirements, and 2/3rds of the ways require actually broadcasting the info. Lets take a look at this new protocol, and poke around at some sample packets (or generate our own).</p>

<p><img src="/assets/drones/FAA_Remote_ID_Options.png" alt="Screenshot from the FAA website showing the 3 ways drones can be compliant with the Remote ID requirement. The drone must: have a standard remote ID built in, have a remote ID broadcaster attached to the drone, or be flying within an FAA-Recognized Identification Area (FRIA), a pre-approved location where Remote ID isn't required" /></p>

<!--more-->

<h2 id="remote-id-basics">Remote ID Basics</h2>

<p>Remote ID is the FAA-required ability to send out a drone’s information while in flight, including things like: current latitude/longitude, some type of ID, pilot location, height, speed, etc. The actual implementation used by drone manufacturers is defined in the ASTM F3411 standard. It gives specifications for drones to send out data in a couple different ways:</p>

<ul>
  <li>Wifi NAN discovery frames</li>
  <li>Wifi Beacon frames</li>
  <li>Bluetooth 4.x advertising beacon messages, called “Legacy Advertising” in the ASTMF3411.</li>
  <li>Bluetooth 5  Extended Advertising messages</li>
</ul>

<p>Since these messages are unencoded, someone with the right software and some wifi and bluetooth dongles can detect these messages. However, in order to do that, you need at least a basic understanding of a lot of different protocols that the remote ID info will be wrapped in, not to mention how the remote ID info is encoded.</p>

<p>So, let’s first see what sort of packets are being sent that contain the remote ID info, then look into the remote ID info itself.</p>

<blockquote>
  <p>If you prefer to just read the specification directly, you can access the ASTM F3411-19 revision <a href="https://thedroneprofessor.com/wp-content/uploads/2022/11/F3411.40165-UAS-Remote-ID.pdf">here</a>, though there has been a newer revision released in 2022. This does not appear to be accessible for free as of this writing.</p>
</blockquote>

<h2 id="wifi">Wifi</h2>

<p>Drones will broadcast on channel 6 (2.437 GHz) in the 2.4 GHz frequency band and may optionally operate in channel 149 (5.745 GHz) in the 5 GHz band. From real-world experience so far, it looks like DJIs will broadcast on 2.4GHz, and Skydio drones are more likely to broadcast solely on 5GHz.</p>

<blockquote>
  <p><strong>NOTE</strong> So if you want to listen for wifi remote ID messages, you have to make sure you’re listening on those specific channels, otherwise you’ll never hear them! This is important, for, say, using wireshark, mentioned below.</p>
</blockquote>

<h3 id="wifi-nan-discovery-frames">Wifi NAN discovery frames</h3>

<p>The NAN discovery frames are actually NAN-specific “public action” Wifi frames. You can narrow this down by looking at the Management Header of a wifi frame first. In there is the “Frame Control” section, which contains the “Type” and “Subtype” fields.To make sure it’s an action frame, you’ll check if the Type is “management” <code class="language-plaintext highlighter-rouge">0x0</code>, and Subtype is “action” <code class="language-plaintext highlighter-rouge">0xD</code>. The “public” part will be verified later in the frame.
After the Frame Control section, there’s a destination address. The NAN Network ID specification calls for the destination address in the header to be <code class="language-plaintext highlighter-rouge">51:6F:9A:01:00:00</code>.</p>

<p>Once it’s confirmed it’s a Wifi NAN action frame, you can confirm the Category ID is “public” <code class="language-plaintext highlighter-rouge">0x4</code> in the NAN Service Discovery Header. We now know it’s a Public Action NAN frame. But we still don’t even know if it’s a drone broadcasting a remote ID packet!</p>

<p>For that, we can check this by looking at the OUI and Service ID inside the frame’s “NAN Service Discovery Header” and its “Service Descriptor Attribute” section. The OUI in the Service Discovery Header is set to the Wifi Alliance OUI, <code class="language-plaintext highlighter-rouge">50:6F:9A</code>.</p>

<blockquote>
  <p>OUI stands for Organizationally Unique Identifier and is a 24-bit number that uniquely identifies a vendor, manufacturer, or other organization</p>
</blockquote>

<p>And in the “Service Descriptor Attribute” section, the Service ID is the first 6 bytes of a sha of <code class="language-plaintext highlighter-rouge">"org.opendroneid.remoteid"</code>, which is <code class="language-plaintext highlighter-rouge">88:69:19:9d:92:09</code>.</p>

<blockquote>
  <p>You can confirm the 6 bytes for the Service ID are correct for yourself by hashing it with some python real quick:</p>
  <div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;&gt;</span> <span class="n">blah</span> <span class="o">=</span> <span class="n">hashlib</span><span class="p">.</span><span class="n">sha256</span><span class="p">(</span><span class="sa">b</span><span class="s">"org.opendroneid.remoteid"</span><span class="p">).</span><span class="n">digest</span><span class="p">()[:</span><span class="mi">6</span><span class="p">]</span>
<span class="o">&gt;&gt;</span> <span class="k">print</span><span class="p">(</span><span class="s">", "</span><span class="p">.</span><span class="n">join</span><span class="p">(</span><span class="nb">hex</span><span class="p">(</span><span class="n">b</span><span class="p">)</span> <span class="k">for</span> <span class="n">b</span> <span class="ow">in</span> <span class="n">blah</span><span class="p">))</span>
<span class="mh">0x88</span><span class="p">,</span> <span class="mh">0x69</span><span class="p">,</span> <span class="mh">0x19</span><span class="p">,</span> <span class="mh">0x9d</span><span class="p">,</span> <span class="mh">0x92</span><span class="p">,</span> <span class="mh">0x9</span>
</code></pre></div>  </div>
</blockquote>

<p>Here’s a screenshot of the F3411 document for the NAN discovery frame. See if you can spot all the values listed above in the frame:</p>

<p><img src="/assets/drones/wifi_nan_discovery_frame.png" alt="screenshot of NAN discovery frame packet diagram" /></p>

<p>Congratulations, you’ve now parsed enough to confirm it’s a Remote ID Wifi message, and you can start processing the info! Jump down farther in this post to see what’s inside. Otherwise, lets switch to a different packet option…</p>

<h3 id="beacon">Beacon</h3>

<p>Another way to send out the remote IDs is via Wifi Beacon frames. These have a different format, and so they need to be parsed differently.</p>

<p>First, take a look at the Management Header of the wifi frame, like last time. Looking in the Frame Control section again, you’ll need to verify the Type is “management” <code class="language-plaintext highlighter-rouge">0x0</code>, Subtype is “Beacon” <code class="language-plaintext highlighter-rouge">0x8</code>.</p>

<p>Now this is where the format changes. Inside the Beacon is several required fields, followed by a series of Information Elements that is basically a series of key/value pairs with info the Beacon wants to share. In our case, we want remote ID beacon info. So we need to look through the beacon’s Information Element “tags” (which are the keys in the key/value idea), for <code class="language-plaintext highlighter-rouge">0xdd</code>, the Vendor Specific Information element type. Then verify the start of the info in that tag (the “value” in the key/value idea) is the ASTM OUI, <code class="language-plaintext highlighter-rouge">FA:0B:BC</code>. We also want to check the type after the OUI is in there and is set to <code class="language-plaintext highlighter-rouge">0x0D</code>, which indicates a Remote ID app code.</p>

<p>Here’s a screenshot with an example of this packet in wireshark. The left side is the parsed packet with the field names and values, and the right side is the raw packet. Note the type/subtype at the top, the Vendor Specific Information Element type, and the OUI listed there.</p>

<p><img src="/assets/drones/pcap_wifi_beacon.png" alt="screenshot of wireshark showing an expanded, parsed beacon packet" /></p>

<blockquote>
  <p><strong>NOTE</strong> If you haven’t done much with wireshark, highly recommend finding some tutorials and poking around in things. It’s amazing for parsing packets of all sorts of protocols, I’ve found endless use for it, from my web dev days, to now.</p>
</blockquote>

<p>The rest of the info in there is a remote ID message, yay!</p>

<h2 id="bluetooth">Bluetooth</h2>

<p>Bluetooth messages are sent over Bluetooth Low Energy (BLE) the most common embedded bluetooth option. The specification supports BLE 4 and 5.</p>

<h3 id="bluetooth-4x">Bluetooth 4.x</h3>
<p>BLE 4 uses advertising beacon messages, called “Legacy Advertising” in the ASTMF3411. Advertising messages are sent on a specific set of channels in the bluetooth frequency range, channels 37, 38, and 39, and the beacon messages are simply sending information into the void (not sending to a particular end point), with a small payload. Which in our case, is the Remote ID messages.</p>

<p>To filter down to the remote ID messages, we need to grab all Advertising beacon messages, and check the “AD info” field. That will contain the Mfg Code, which should be <code class="language-plaintext highlighter-rouge">0xFFFA</code>, the ASTM code. It will also contain the “AD App” field, which should be <code class="language-plaintext highlighter-rouge">0x0D</code>, for remote ID. The content in this advertising message will be what we want. Here’s another screenshot from the ASTM F3411 spec, showing those fields in the packet:</p>

<p><img src="/assets/drones/ble4_beacon_frame.png" alt="screenshot of ble beacon frame packet diagram" /></p>

<h3 id="bluetooth-5">Bluetooth 5</h3>
<p>Bluetooth 5 Extended Advertisements messages, when sent on a LE Coded (S=8) PHY, can increase the range of the advertisements by a factor of 4, which is why it’s so nice. It achieves this by cutting the rate of the data transfer down (with S=8, data rate is 1/8th), where multiple symbols make up 1 bit (with S=8… it’s eight symbols per bit), this lets the receiver parse things from a farther distance.</p>

<p>These Extended advertisements also use a two-step advertisement messages, because it uses the advertisement types <code class="language-plaintext highlighter-rouge">ADV_EXT_IND</code> and <code class="language-plaintext highlighter-rouge">AUX_ADV_IND</code>. So  they send advertisements on the normal advertising channels like BLE 4, but those advertisements actually point to larger advertisement packets on other bluetooth channels. This second, larger advertisement frame will have the actual remote ID information. Like the BLE 4 protocol, it will contain the Mfg Code, which should be <code class="language-plaintext highlighter-rouge">0xFFFA</code> and the “AD App” field, which should be <code class="language-plaintext highlighter-rouge">0x0D</code>.</p>

<p>Because the secondary advertisement frame is larger, the remote ID information will be sent as a “message pack” type, which combines different remote ID messages into one large packet.</p>

<p><img src="/assets/drones/ble5_beacon_frame.png" alt="screenshot of the secondary ble advertising packet, which has a larger payload" /></p>

<p>What are these “message pack” and other remote ID message types? I’m glad you asked…</p>

<h2 id="remote-id-messages">Remote ID messages</h2>

<p>So after all the parsing above, you’ve finally gotten a Remote ID message. But what’s in that message? There’s several different type of messages according to the full ASTM F3411:</p>

<p><img src="/assets/drones/RemoteID_Types.png" alt="Table showing the types of remote ID messages: Basic ID, Location/Vector, Authentication, Self-ID, System, Operator ID, Message Pack" /></p>

<p>The main ones we care about when detecting drones though, are just a subset: Basic ID, Location/Vector, System, and Message Pack. Each carry a different subset of data, but can be combined to give a full overview of what the drone and operator are doing.</p>

<h3 id="message-pack">Message Pack</h3>

<p>This one is easy - it’s just a collection of the other messages, so adding support for this is just allowing there to be multiple messages inside a single sent frame (however it was sent). Used in BLE 5 extended advertising messages, since they have the room for it.</p>

<h3 id="basic-id">Basic ID</h3>

<p>This gives the ID for the drone, and its ID type. E.g. DJI drones will broadcast their Serial numbers as their ID, with ID Type of, you guessed it, “Serial Number (ANSI/CTA-2063-A)”. It will also give the type of drone it is, e.g. fixed wing, helicopter/multirotor, etc. Sadly, I have not spotted any drones listing their type as “Airship (such as a blimp)”, but maybe someday.</p>

<h3 id="locationvector">Location/Vector</h3>

<p>This message gives info about the location, altitude, direction, and speed of the drone. One interesting thing to note about altitude, there’s a AGL height which is height Above Ground Level. This references from the drone’s take-off point, and may not actually be accurate to altitude above the ground where it is actively flying.</p>

<h3 id="system">System</h3>

<p>This message gives the operator’s latitude and longitude. Depending on how things are setup, this will just default to the drone’s take-off location, OR it will be updated dynamically.</p>

<h2 id="using-this-info---wireshark">Using This Info - Wireshark</h2>

<p>You’ve learned a bunch of protocol things, but what can you actually <em>do</em> with it? First and simplest option is to setup a laptop with <a href="https://www.wireshark.org/download.html">wireshark</a> and <a href="https://github.com/opendroneid/wireshark-dissector/tree/main">a custom protocol dissector</a> so you can actually capture the packets you just learned about above yourself! You’ll need a laptop that can run wireshark, a wifi or bluetooth dongle to capture the packets, and a drone that broadcasts its Remote ID (e.g. a DJI Neo).</p>

<blockquote>
  <p>If you want to just play around with packets, the dissector comes with a few pcaps you can load and poke around at. So you would only have to install wireshark and add the dissector. E.g. <a href="https://github.com/opendroneid/wireshark-dissector/blob/main/odid_wifi_sample.pcap">https://github.com/opendroneid/wireshark-dissector/blob/main/odid_wifi_sample.pcap</a></p>
</blockquote>

<p>The basic steps for this are:</p>

<ul>
  <li>Install Wireshark, setup so non-root users can capture packets</li>
  <li>Install <a href="(https://github.com/opendroneid/wireshark-dissector/tree/main)">custom protocol dissector</a>, to parse Remote IDs</li>
  <li>Plug a wifi or bluetooth dongle into your computer
    <ul>
      <li>if it’s a wifi dongle, make sure it supports “monitor” mode and set the 2.4GHz channel to “6”</li>
      <li>other setups can be found in the readme of the protocol dissector</li>
    </ul>
  </li>
  <li>Run wireshark</li>
  <li>Fly a drone that broadcasts Remote ID on the channel you’re lisenting on (e.g. 2.4GHz channel 6)</li>
  <li>Capture packets</li>
  <li>Profit!</li>
</ul>

<p>On my linux laptop, these commands were useful to setup my wifi dongle, a <a href="https://www.pandawireless.com/pandaAXE3000dualant.html">Panda PAU0F</a> to monitor mode on channel 6. Note the interface name <code class="language-plaintext highlighter-rouge">wlx9cefd5f644bd</code> will be different for you, determine by simply running <code class="language-plaintext highlighter-rouge">sudo ifconfig</code> with the wifi dongle unplugged and then plugged in, and see what interface appears. These commands: take the interface down so I can modify the configuration, set the monitor flag, bring the interface back up, and set it to monitor channel 6.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>ifconfig wlx9cefd5f644bd down
<span class="nb">sudo </span>iw wlx9cefd5f644bd <span class="nb">set </span>monitor none
<span class="nb">sudo </span>ifconfig wlx9cefd5f644bd up

<span class="nb">sudo </span>iw dev wlx9cefd5f644bd <span class="nb">set </span>channel 6
</code></pre></div></div>

<blockquote>
  <p><strong>NOTE</strong> not all wifi dongles can be set to monitor mode! You’ll have to research before you buy. Acrylic wifi has a list of monitor mode dongles that may work with your computer <a href="https://www.acrylicwifi.com/en/wifi-analyzer/requirements-and-compatibility/">here</a></p>
</blockquote>

<h2 id="conclusion">Conclusion</h2>

<p>Hopefully this whirlwind overview of the protocols involved in Remote IDs can give you better context for what you’d want to look for when detecting drones via Remote IDs. My next post will most likely be an overview of currently existing drone detection software with their pros and cons, as well as what work I’ve done personally to expand those options.</p>

<h2 id="references">References</h2>

<p>Open Drone ID library: <a href="https://github.com/opendroneid/opendroneid-core-c">https://github.com/opendroneid/opendroneid-core-c</a></p>

<p>IEEE article with an overview of drone tracking technology and regulations: <a href="https://ieeexplore.ieee.org/document/9861637">https://ieeexplore.ieee.org/document/9861637</a></p>

<p>ASTM F3411 Wi-Fi Beacon protocol doc <a href="https://thedroneprofessor.com/wp-content/uploads/2022/11/F3411.40165-UAS-Remote-ID.pdf">https://thedroneprofessor.com/wp-content/uploads/2022/11/F3411.40165-UAS-Remote-ID.pdf</a></p>

<p>Setting a wifi interface to monitor mode: <a href="https://www.geeksforgeeks.org/linux-unix/how-to-put-wifi-interface-into-monitor-mode-in-linux/">https://www.geeksforgeeks.org/linux-unix/how-to-put-wifi-interface-into-monitor-mode-in-linux/</a></p>

<p>Wireshark dissector of drone IDs <a href="https://github.com/opendroneid/wireshark-dissector/tree/main">https://github.com/opendroneid/wireshark-dissector/tree/main</a></p>

<p>Monitor mode wifi dongles: <a href="https://www.acrylicwifi.com/en/wifi-analyzer/requirements-and-compatibility/">https://www.acrylicwifi.com/en/wifi-analyzer/requirements-and-compatibility/</a></p>]]></content><author><name></name></author><category term="technical write up" /><category term="side project" /><category term="How-to" /><category term="side project" /><category term="embedded" /><category term="remote ID" /><summary type="html"><![CDATA[Drones have gotten more and more popular lately, from filming fun things on social media, delivering packages for Amazon, being used by law enforcements in the states, all the way to being used in the ukraine war. Understanding the technology and building tech to track the drones seems very useful. We’ve had public tracking of airplanes via ADSB for years at this point (see my previous post about setting up your own flight tracker with ADSB), but no equivalent with drones. Partially this is because drones, up until recently, didn’t really have ways to track them, unless you were visually tracking them or doing some advanced radio frequency shenanigans. That has changed relatively recently (in 2023) when the FAA started requiring Remote IDs for drones, the functional equivalent to the broadcast ADSB info, but for drones specifically. Specific information about the drone, its location, and the drone operator are broadcast in one of the variety of ways specified by the standard put out by the ASTM (a standards governing body), which is charmingly named “F3411”. With this new ability, it’s possible to start tracking and aggregating drone flight information, same as ADSB. As the FAA website shows (screenshot below), there are now several ways to be compliant with the remote ID requirements, and 2/3rds of the ways require actually broadcasting the info. Lets take a look at this new protocol, and poke around at some sample packets (or generate our own).]]></summary></entry><entry><title type="html">Have E-ink; Will Travel - Serial comms with raspberry pi and e-ink</title><link href="/blog/2026/5/14/raspberry-pi-and-eink" rel="alternate" type="text/html" title="Have E-ink; Will Travel - Serial comms with raspberry pi and e-ink" /><published>2026-05-14T00:00:00+00:00</published><updated>2026-05-14T00:00:00+00:00</updated><id>/blog/2026/5/14/raspberry-pi-and-eink</id><content type="html" xml:base="/blog/2026/5/14/raspberry-pi-and-eink"><![CDATA[<ul id="markdown-toc">
  <li><a href="#hardware-needed" id="markdown-toc-hardware-needed">Hardware Needed</a>    <ul>
      <li><a href="#why-e-ink" id="markdown-toc-why-e-ink">Why e-ink?</a></li>
    </ul>
  </li>
  <li><a href="#setup-and-verification" id="markdown-toc-setup-and-verification">Setup And Verification</a>    <ul>
      <li><a href="#hardware-setup" id="markdown-toc-hardware-setup">Hardware setup</a></li>
      <li><a href="#software" id="markdown-toc-software">Software</a></li>
    </ul>
  </li>
  <li><a href="#python-script" id="markdown-toc-python-script">Python Script</a>    <ul>
      <li><a href="#finding-the-serial-device" id="markdown-toc-finding-the-serial-device">Finding the serial device</a>        <ul>
          <li><a href="#troubleshooting-finding-your-device" id="markdown-toc-troubleshooting-finding-your-device">Troubleshooting Finding Your Device</a></li>
        </ul>
      </li>
      <li><a href="#reformatting-text" id="markdown-toc-reformatting-text">Reformatting text</a></li>
    </ul>
  </li>
  <li><a href="#systemd" id="markdown-toc-systemd">Systemd</a></li>
  <li><a href="#future-improvements" id="markdown-toc-future-improvements">Future improvements</a></li>
  <li><a href="#conclusion" id="markdown-toc-conclusion">Conclusion</a></li>
  <li><a href="#references" id="markdown-toc-references">References</a></li>
  <li><a href="#full-code" id="markdown-toc-full-code">Full Code</a>    <ul>
      <li><a href="#systemd-1" id="markdown-toc-systemd-1">Systemd</a></li>
      <li><a href="#bash-script" id="markdown-toc-bash-script">Bash script</a></li>
      <li><a href="#python-script-1" id="markdown-toc-python-script-1">Python Script</a></li>
    </ul>
  </li>
</ul>

<p>Playing around with different development projects, there’s often a need to listen on a USB serial port connection to see log outputs. If I’m making a device that I want to test out in the wild, without a full display yet, it’d be nice to have a grab-and-go tool for that. So, I decided to put together a raspberry pi zero I had lying around, plus the <a href="https://www.adafruit.com/product/4687">adafruit e-ink bonnet</a> and a powerbank, to let me read serial logs on the road.</p>

<p><img src="/assets/raspberry-pi-and-eink/e-ink-setup.jpg" alt="Picture of the raspberry pi with the E-ink screen connected, displaying a timestamp and &quot;Device is active and scanning&quot;. It is connected to a white powerbank, and a Xiao ESP32S3 soldered onto perfboard with a piezo buzzer" /></p>

<!--more-->

<blockquote>
  <p><strong>Note before I get into it</strong> If you want to be <em>boring</em> (aka practical), you should check out phone apps that allow you to read serial info from devices connected to your phone, that may be a simpler solution than buying more hardware. My way is more fun though.</p>
</blockquote>

<h2 id="hardware-needed">Hardware Needed</h2>

<p>Below is the hardware I used for development and my final product. Net price for this, if you don’t have things lying around already, will probably run 65-75 bucks. The power bank was the spendiest buy, at 27 dollars, so if you already have one that would work, that helps drop the price down.</p>

<ul>
  <li><a href="https://www.adafruit.com/product/4687">adafruit e-ink bonnet</a></li>
  <li><a href="https://www.adafruit.com/product/4288">powerbank capable of 5V output</a></li>
  <li><a href="https://www.adafruit.com/product/2885">raspberry pi zero</a>
    <ul>
      <li><strong>NOTE</strong> for the zero, it doesn’t have wifi, so you need to make sure you have the right tools for development. I used:
        <ul>
          <li><a href="https://www.adafruit.com/product/1099">USB OTG Host Cable - MicroB OTG male to A female</a></li>
          <li>a powered USB hub</li>
          <li><a href="https://www.adafruit.com/product/1012">A wifi dongle of some sort</a></li>
        </ul>
      </li>
      <li>for ease of development, if you’re buying new, it would make sense to instead by a <a href="https://www.adafruit.com/product/3708">raspberry pi zero W</a>, so you can more easily connect to the pi via wifi/ssh</li>
    </ul>
  </li>
  <li>power cord for the pi zero, for during development</li>
  <li>if you want to keep the pi protected, I found the <a href="https://www.adafruit.com/product/3252">adafruit pi zero case</a> fits well with the e-ink screen on top</li>
  <li>micro USB cable that can connect to whatever projects you’re working on (in my case, I needed a micro USB to USB C)</li>
  <li>SD card for the pi’s OS</li>
  <li>Micro-hdmi adaptor so you can connect to a monitor, and a keyboard and mouse for the pi
    <ul>
      <li><em>Technically</em> if you do everything right during initial setup, you don’t need this. <em>In the real world</em> it’s good to have everything for troubleshooting during your initial Pi setup.</li>
    </ul>
  </li>
</ul>

<h3 id="why-e-ink">Why e-ink?</h3>

<p>Cuz it’s fun, honestly. A common argument for e-inks is power saving, since you don’t have to give it power to keep what was last on the screen, but I haven’t profiled that so I can’t say for sure. I really like the visual look of e-inks, and want to work on future projects with e-inks, so I figured this would be an easy project to do some basic testing with an e-ink screen.</p>

<p>A common “con” of e-inks is the refresh rate, a lot of them have miserable refresh rates if you want real-time or near-real-time responses. For the sort of projects I work on, I’m not getting spammed by serial logs, so the slower timing isn’t a problem. And from my testing, this e-ink screen has pretty slow refresh. I suspect the slow refresh could be improved if I dug into the docs more and looked for some time-saving hacks either in the python library from adafruit or how I’m displaying things, but for now, I’m just keeping things simple.</p>

<h2 id="setup-and-verification">Setup And Verification</h2>

<h3 id="hardware-setup">Hardware setup</h3>

<p>Put the pi in the case if you’re using one, then connect the e-ink bonnet to the raspberry pi. Make sure it’s fully seated, I had some weird issues with the display not working that turned out to be because I hadn’t pushed them together hard enough.</p>

<p>Plug in the OTG cable, hook it up to your powered USB hub, and plug in your wifi dongle, and if you want to do setup using the pi’s GUI, connect a keyboard and mouse here.</p>

<h3 id="software">Software</h3>

<p>Flash the latest version of the raspberry pi OS for your zero, using the Raspberry Pi Imager program. You’ll also want to setup your username and password and enable SSH. If you have a pi zero with no wifi, you’ll want to setup a keyboard and monitor so you can configure the wifi via the wifi dongle. Once the initial setup is done, you should be able to just remote in via ssh and do future steps over that. If you haven’t had experience setting up a pi before, <a href="https://www.raspberrypi.com/documentation/computers/getting-started.html">the official docs</a> are pretty good.</p>

<p>Install the blinka library from adafruit. They have good instructions <a href="https://learn.adafruit.com/circuitpython-on-raspberrypi-linux/installing-circuitpython-on-raspberry-pi">here</a>, that involve setting up a virtual environment and adding their blinka python script to the <code class="language-plaintext highlighter-rouge">$PATH</code> environment variable.</p>

<p>Once you have your virtual environment setup, you’ll also want to install the EPD library and the pillow library with this:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>pip3 install adafruit-circuitpython-epd
sudo apt-get install python3-pil
</code></pre></div></div>

<p>To validate everything works, you can run the REPL (<code class="language-plaintext highlighter-rouge">python3</code>) on your pi and run the following script, it should print a “Hello world” message on the e-ink screen, and you can then write more to the screen by just calling the <code class="language-plaintext highlighter-rouge">display_text</code> function, e.g. <code class="language-plaintext highlighter-rouge">display_text("my new string!")</code></p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">board</span>
<span class="kn">import</span> <span class="nn">busio</span>
<span class="kn">import</span> <span class="nn">digitalio</span>
<span class="kn">from</span> <span class="nn">PIL</span> <span class="kn">import</span> <span class="n">Image</span><span class="p">,</span> <span class="n">ImageDraw</span><span class="p">,</span> <span class="n">ImageFont</span>
<span class="kn">from</span> <span class="nn">adafruit_epd.epd</span> <span class="kn">import</span> <span class="n">Adafruit_EPD</span>
<span class="kn">import</span> <span class="nn">serial</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">import</span> <span class="nn">serial.tools.list_ports</span>


<span class="kn">from</span> <span class="nn">adafruit_epd.ssd1680</span> <span class="kn">import</span> <span class="n">Adafruit_SSD1680Z</span>


<span class="c1"># First define some color constants
</span><span class="n">WHITE</span> <span class="o">=</span> <span class="p">(</span><span class="mh">0xFF</span><span class="p">,</span> <span class="mh">0xFF</span><span class="p">,</span> <span class="mh">0xFF</span><span class="p">)</span>
<span class="n">BLACK</span> <span class="o">=</span> <span class="p">(</span><span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">)</span>

<span class="c1"># Next define some constants to allow easy resizing of shapes and colors
</span><span class="n">BORDER</span> <span class="o">=</span> <span class="mi">5</span>
<span class="n">FONTSIZE</span> <span class="o">=</span> <span class="mi">18</span>
<span class="n">FONT_MAX_LENGTH</span> <span class="o">=</span> <span class="mi">27</span>
<span class="n">BACKGROUND_COLOR</span> <span class="o">=</span> <span class="n">WHITE</span>
<span class="n">TEXT_COLOR</span> <span class="o">=</span> <span class="n">BLACK</span>


<span class="c1"># create the spi device and pins we will need
</span><span class="n">spi</span> <span class="o">=</span> <span class="n">busio</span><span class="p">.</span><span class="n">SPI</span><span class="p">(</span><span class="n">board</span><span class="p">.</span><span class="n">SCK</span><span class="p">,</span> <span class="n">MOSI</span><span class="o">=</span><span class="n">board</span><span class="p">.</span><span class="n">MOSI</span><span class="p">,</span> <span class="n">MISO</span><span class="o">=</span><span class="n">board</span><span class="p">.</span><span class="n">MISO</span><span class="p">)</span>
<span class="n">ecs</span> <span class="o">=</span> <span class="n">digitalio</span><span class="p">.</span><span class="n">DigitalInOut</span><span class="p">(</span><span class="n">board</span><span class="p">.</span><span class="n">CE0</span><span class="p">)</span>
<span class="n">dc</span> <span class="o">=</span> <span class="n">digitalio</span><span class="p">.</span><span class="n">DigitalInOut</span><span class="p">(</span><span class="n">board</span><span class="p">.</span><span class="n">D22</span><span class="p">)</span>
<span class="n">rst</span> <span class="o">=</span> <span class="n">digitalio</span><span class="p">.</span><span class="n">DigitalInOut</span><span class="p">(</span><span class="n">board</span><span class="p">.</span><span class="n">D27</span><span class="p">)</span>
<span class="n">busy</span> <span class="o">=</span> <span class="n">digitalio</span><span class="p">.</span><span class="n">DigitalInOut</span><span class="p">(</span><span class="n">board</span><span class="p">.</span><span class="n">D17</span><span class="p">)</span>
<span class="n">srcs</span> <span class="o">=</span> <span class="bp">None</span>


<span class="c1"># e ink display
</span><span class="n">display</span> <span class="o">=</span> <span class="n">Adafruit_SSD1680Z</span><span class="p">(</span><span class="mi">122</span><span class="p">,</span> <span class="mi">250</span><span class="p">,</span> <span class="n">spi</span><span class="p">,</span> <span class="n">cs_pin</span><span class="o">=</span><span class="n">ecs</span><span class="p">,</span> <span class="n">dc_pin</span><span class="o">=</span><span class="n">dc</span><span class="p">,</span> <span class="n">sramcs_pin</span><span class="o">=</span><span class="n">srcs</span><span class="p">,</span>
                          <span class="n">rst_pin</span><span class="o">=</span><span class="n">rst</span><span class="p">,</span> <span class="n">busy_pin</span><span class="o">=</span><span class="n">busy</span><span class="p">)</span>


<span class="n">display</span><span class="p">.</span><span class="n">rotation</span> <span class="o">=</span> <span class="mi">1</span>

<span class="c1"># re-draw the entire screen with the new text message
</span><span class="k">def</span> <span class="nf">display_text</span><span class="p">(</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="n">image</span> <span class="o">=</span> <span class="n">Image</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="s">"RGB"</span><span class="p">,</span> <span class="p">(</span><span class="n">display</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">display</span><span class="p">.</span><span class="n">height</span><span class="p">),</span> <span class="n">WHITE</span><span class="p">)</span>

    <span class="c1"># Get drawing object to draw on image.
</span>    <span class="n">draw</span> <span class="o">=</span> <span class="n">ImageDraw</span><span class="p">.</span><span class="n">Draw</span><span class="p">(</span><span class="n">image</span><span class="p">)</span>

    <span class="c1"># Draw a filled box as the background
</span>    <span class="n">draw</span><span class="p">.</span><span class="n">rectangle</span><span class="p">(</span>
        <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">display</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">display</span><span class="p">.</span><span class="n">height</span> <span class="o">-</span> <span class="mi">1</span><span class="p">),</span>
        <span class="n">fill</span><span class="o">=</span><span class="n">BACKGROUND_COLOR</span><span class="p">,</span>
    <span class="p">)</span>

    <span class="c1"># Load a TTF Font
</span>    <span class="c1"># NOTE: linux systems normally default have this font
</span>    <span class="n">font</span> <span class="o">=</span> <span class="n">ImageFont</span><span class="p">.</span><span class="n">truetype</span><span class="p">(</span><span class="s">"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"</span><span class="p">,</span> <span class="n">FONTSIZE</span><span class="p">)</span>

    <span class="c1"># Draw the text
</span>    <span class="n">draw</span><span class="p">.</span><span class="n">text</span><span class="p">(</span>
    <span class="p">(</span><span class="n">BORDER</span><span class="p">,</span> <span class="n">BORDER</span><span class="p">),</span>
    <span class="n">text</span><span class="p">,</span>
    <span class="n">font</span><span class="o">=</span><span class="n">font</span><span class="p">,</span>
    <span class="n">fill</span><span class="o">=</span><span class="n">TEXT_COLOR</span><span class="p">,</span>
    <span class="p">)</span>

    <span class="c1"># Display image.
</span>    <span class="n">display</span><span class="p">.</span><span class="n">image</span><span class="p">(</span><span class="n">image</span><span class="p">)</span>
    <span class="n">display</span><span class="p">.</span><span class="n">display</span><span class="p">()</span>

<span class="n">display_text</span><span class="p">(</span><span class="s">"Hello World!"</span><span class="p">)</span>
</code></pre></div></div>

<h2 id="python-script">Python Script</h2>

<p>This script is the meat of the project. The basic functionality it sets up is:</p>

<ul>
  <li>Configure and connect to the Adafruit E-ink screen using circuit python and adafruit’s libraries</li>
  <li>Constantly listen on a given serial port for any text
    <ul>
      <li>Currently the script hard codes which USB device to listen to, and what baudrate/serial configuration it has</li>
    </ul>
  </li>
  <li>Given the text, re-format it a bit to make sure it doesn’t run off the screen of the e-ink, and add a timestamp
    <ul>
      <li>It also checks if it’s receiving the exact same text repeatedly (e.g. a heartbeat message from the device). In this case, it will only update the e-ink screen if it’s been longer than a minute, so the timestamp gets updated and you know the script is still working.</li>
    </ul>
  </li>
  <li>Using the Pillow library, draw a new image for the e-ink screen with the re-formatted text, then send it to the e-ink to display</li>
</ul>

<p>You can directly run the script on the pi by copying it over and then using <code class="language-plaintext highlighter-rouge">ssh</code> to run.</p>

<blockquote>
  <p><strong>Don’t Forget</strong> You’ll need to activate your python virtual environment before you try running it though!</p>
</blockquote>

<p>As it’s just shy of 175 lines, I’ll just include the full script below in the <a href="#full-code">Full Code</a> section, for you to take a closer look.</p>

<p>I do want to point out some interesting pieces of the script.</p>

<h3 id="finding-the-serial-device">Finding the serial device</h3>

<p>Finding the device I want to listen to ends up being more complicated than you may think. If, for example, I’m using a <a href="https://www.seeedstudio.com/Seeed-Studio-XIAO-ESP32S3-3PCS-p-5919.html">Xiao ESP32 device</a>, I can plug in a USB C to micro USB cable to it and the raspberry pi, and it will be automounted at a standard <code class="language-plaintext highlighter-rouge">/dev/tty&lt;blah&gt;</code> (e.g. <code class="language-plaintext highlighter-rouge">/dev/ttyACM0</code>). <em>However</em>, the exact number of this ESP32 can change from one boot up to another. So, if I just hardcode the serial connection, e.g. <code class="language-plaintext highlighter-rouge">serial.Serial('/dev/ttyACM0', 115200, timeout=1)</code>, the script will occasonally fail.</p>

<p>My workaround for this is to use the Product ID and Vendor ID (PID and VID) and the <code class="language-plaintext highlighter-rouge">serial.tools.list_ports</code> functionality in python. The PID and VID are part of USB standard ways of identifying devices, the Vendor ID is assigned to a particular company by the USB Committee, and the Product ID is assigned by the company to a particular USB device.</p>

<p>With this, I can look through connected USB devices and filter down to only devices that are specifically an Espressif USB JTAG/serial connection, and connect to the first one I find. Since I only ever have one ESP32 connected, it works great.</p>

<p>After I wrote this all up, I did some extra searching online, and did see some people solved this by <a href="https://loketdiversen.nl/2015/02/howto-persistent-device-names-on-raspberry-pi/">creating <code class="language-plaintext highlighter-rouge">udev</code> rules</a> to create permanent <code class="language-plaintext highlighter-rouge">/dev/</code> mount points for their USB devices. The trade off here is that you have to do more customizing of the raspberry pi OS. I was trying to keep this to a minimum to make it easy to create more of these setups in the future, so I’m satisified with the python solution for now.</p>

<h4 id="troubleshooting-finding-your-device">Troubleshooting Finding Your Device</h4>

<p>If you want to use this script, and you’re having issues with getting the right PID and VID, or you’re unsure if python is actually seeing your device, this lil python script, run in the REPL can help you out:</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="kn">import</span> <span class="nn">serial</span>
<span class="kn">import</span> <span class="nn">serial.tools.list_ports</span>
<span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">serial</span><span class="p">.</span><span class="n">tools</span><span class="p">.</span><span class="n">list_ports</span><span class="p">.</span><span class="n">comports</span><span class="p">():</span>
    <span class="k">print</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">pid</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">vid</span><span class="p">)</span>
    <span class="k">print</span><span class="p">(</span><span class="n">p</span><span class="p">.</span><span class="n">manufacturer</span><span class="p">)</span>
</code></pre></div></div>

<p>Running the <code class="language-plaintext highlighter-rouge">lsusb</code> command can also help, to make sure the pi is seeing your device, and it also gives your pid/vid in hex</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">&gt;</span> lsusb
Bus 001 Device 006: ID 303a:1001 Espressif USB JTAG/serial debug unit
</code></pre></div></div>

<p>Checking in multiple different ways can narrow down which part of your system is currently acting up. E.g. if python isn’t showing your device but the <code class="language-plaintext highlighter-rouge">lsusb</code> is, maybe there’s an issue with your python installation. But if the python script <em>and</em> <code class="language-plaintext highlighter-rouge">lsusb</code> cant see it, maybe it’s an issue with your device, or the physical port.</p>

<blockquote>
  <p><strong>Fun Fact</strong> testing in multiple ways to narrow down which “thing” is breaking, is a super valuable skill for firmware development, software development, and general life. (insert “the more you know” banner here)</p>
</blockquote>

<h3 id="reformatting-text">Reformatting text</h3>

<p>To keep any text I get from the serial device from running off the right side of the e-ink screen, I added a function that replaces spaces with the newline character <code class="language-plaintext highlighter-rouge">\n</code> whenever the text starts getting too long. This feels like a nice way to display the text without getting weird cutoffs, though it depends on the type of text you’re getting from your serial device.</p>

<p>I also currently dont have it check to see if the text is too long and will go off the bottom of the screen. My current use doesn’t need that functionality, so why add it? If I do, I’ll most likely update the script to do more of a buffer queue system, to support faster serial logs as well as longer messages.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="k">def</span> <span class="nf">reformat_text</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">maxlength</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
    <span class="n">new_string</span> <span class="o">=</span> <span class="s">""</span>
    <span class="n">cur_row_length</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">text</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">' '</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">cur_row_length</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">maxlength</span><span class="p">:</span>
            <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">new_string</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
                <span class="n">new_string</span> <span class="o">=</span> <span class="n">word</span>
                <span class="n">cur_row_length</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="n">new_string</span> <span class="o">=</span> <span class="n">new_string</span> <span class="o">+</span> <span class="s">" "</span> <span class="o">+</span> <span class="n">word</span>
                <span class="n">cur_row_length</span> <span class="o">+=</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">new_string</span> <span class="o">=</span> <span class="n">new_string</span> <span class="o">+</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span> <span class="o">+</span> <span class="n">word</span>
            <span class="n">cur_row_length</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">new_string</span>
</code></pre></div></div>

<h2 id="systemd">Systemd</h2>

<p>Once I have the python script working, I want to run it automatically on every boot. This means I will have to dip my toes into the <code class="language-plaintext highlighter-rouge">systemd</code> world.</p>

<p>Systemd manages services on linux, where services are just long-running programs that can be configured to run at certain times, or in my case, on boot, and then forever. To tell Systemd I have a new program I want it to handle, I have to make a service configuration file in the right location that it’ll look at, and tell it I want it to boot, and if my program crashes out, have it restart it.</p>

<p>The following Systemd service configuration file, does just that, when saved at <code class="language-plaintext highlighter-rouge">/lib/systemd/system/shellscript.service</code>.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>Unit]  
<span class="nv">Description</span><span class="o">=</span>My Python Autostart Script  
<span class="nv">After</span><span class="o">=</span>multi-user.target  
 
<span class="o">[</span>Service]  
<span class="nv">User</span><span class="o">=</span>daniellet
<span class="nv">WorkingDirectory</span><span class="o">=</span>/home/daniellet
<span class="nv">ExecStart</span><span class="o">=</span>/home/daniellet/start_script.sh  
<span class="nv">Restart</span><span class="o">=</span>always  
<span class="nv">RestartSec</span><span class="o">=</span>5  
 
<span class="o">[</span>Install]  
<span class="nv">WantedBy</span><span class="o">=</span>multi-user.target 
</code></pre></div></div>
<p>I created my python file and saved it in my user’s home directory, which is why I tell systemd to run as me, in my <code class="language-plaintext highlighter-rouge">/home/daniellet</code> directory. The <code class="language-plaintext highlighter-rouge">ExecStart</code> tells it what script to run. But you may notice, that script isn’t my python script! Instead, it’s a trusty bash script, what’s up with that!</p>

<p>We can’t run the python script directly simply because the python script is running in a virtual environment, and you will need to activate the virtual environment every time the Pi is rebooted. So this bash script, saved as <code class="language-plaintext highlighter-rouge">start_script.sh</code>, does just that. Create it in your home directory, and be sure to run a <code class="language-plaintext highlighter-rouge">chmod +x</code> to make it executable after you make it.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">source env</span>/bin/activate
python3 <span class="nt">-u</span> printing_to_eink.py
</code></pre></div></div>
<p>The <code class="language-plaintext highlighter-rouge">-u</code> flag on the python command is actually important. My python script prints logging info to standard out. Systemd has built in support for logging standard out info from the services it runs, but python buffers the standard out, and when testing, was resulting in the logging output not actually getting logged. The solution was to add the <code class="language-plaintext highlighter-rouge">-u</code> flag, which makes it unbuffered and output right away.</p>

<p>Once the bash script and config file are made, you’ll need to reload the <code class="language-plaintext highlighter-rouge">systemd</code> system so it re-looks for configuration files and sees my new service. You can do this with <code class="language-plaintext highlighter-rouge">sudo systemctl daemon-reload </code>. Now you can start using the systemd commands to mess with your new service! The service name will be the name of the configuration file you made (in my case, <code class="language-plaintext highlighter-rouge">shellscript.service</code>)</p>

<p>Here are some useful commands around systemd for troubleshooting your setup</p>

<p>Manually start/stop the service:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl start shellscript.service
<span class="nb">sudo </span>systemctl stop shellscript.service
</code></pre></div></div>

<p>Check Current Status of the service:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl status shellscript.service
</code></pre></div></div>

<p>Toggle if the service runs at boot:</p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="nb">sudo </span>systemctl <span class="nb">enable </span>shellscript.service
<span class="nb">sudo </span>systemctl disable shellscript.service
</code></pre></div></div>

<p>Now, you can also see the logging output from my python script by using <code class="language-plaintext highlighter-rouge">journalctl</code>. When it’s running, you can use the following command to see the logs:</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code>journalctl <span class="nt">-u</span> shellscript.service
</code></pre></div></div>
<p><img src="/assets/raspberry-pi-and-eink/journalctl-screenshot.png" alt="screenshot of the output of journalctl command, showing all print statements from the printing_to_eink.py script" /></p>

<p>With the service now enabled, you should be able to unplug and re-plug in the power to the pi zero, and the e-ink screen should display a “Booting up…” message, followed by the serial output from your device! You did it!</p>

<blockquote>
  <p><strong>NOTE</strong> The initial bootup time on my pi is pretty dang slow, so be patient!</p>
</blockquote>

<h2 id="future-improvements">Future improvements</h2>

<p>Some ideas for future improvements that may or may not come to pass:</p>

<ul>
  <li>Update the script to not hard-code the PID/VID and baudrate
    <ul>
      <li>or update to have hardcoded udev rules so the script doesn’t have to hunt for the serial connection</li>
    </ul>
  </li>
  <li>make a nifty enclosure for it, either 3D printed or some other way</li>
  <li>Currently the timestamp is what it thinks is central time, but since it doesn’t have an internal battery, when I pull the power, the clock doesn’t stay up to date. And with no internal wifi, it doesn’t update time from the internet. So I’d like to either add an RTC battery, or update the timestamp to simply be time from boot</li>
</ul>

<h2 id="conclusion">Conclusion</h2>

<p>Setting up the e-ink screen and getting it running for a basic use like this was easier than I had expected. I think I probably spent more time making sure my systemd script was setup correctly than writing the python script. And I did end up using this setup a couple times while testing other devices, so it wasn’t <em>just</em> a fun project with no application. I’ll definitely be keeping this e-ink screen in mind for future projects.</p>

<h2 id="references">References</h2>

<p>Adafruit’s e-ink guide: <a href="https://learn.adafruit.com/2-13-in-e-ink-bonnet">https://learn.adafruit.com/2-13-in-e-ink-bonnet</a></p>

<p>Setting up circuitpython blinka library:
<a href="https://learn.adafruit.com/circuitpython-on-raspberrypi-linux/installing-circuitpython-on-raspberry-pi">https://learn.adafruit.com/circuitpython-on-raspberrypi-linux/installing-circuitpython-on-raspberry-pi</a></p>

<p>Setting udev rules on raspberry pi: <a href="https://loketdiversen.nl/2015/02/howto-persistent-device-names-on-raspberry-pi/">https://loketdiversen.nl/2015/02/howto-persistent-device-names-on-raspberry-pi/</a></p>

<p>Setting up systemd on linux: <a href="https://tecadmin.net/run-shell-script-as-systemd-service/">https://tecadmin.net/run-shell-script-as-systemd-service/</a></p>

<h2 id="full-code">Full Code</h2>

<p>Below is the full code for each part of this project. The Systemd configuration and bash file, and the python script.</p>

<h3 id="systemd-1">Systemd</h3>

<p>systemd service configuration file, saved at <code class="language-plaintext highlighter-rouge">/lib/systemd/system/shellscript.service</code></p>
<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="o">[</span>Unit]  
<span class="nv">Description</span><span class="o">=</span>My Python Autostart Script  
<span class="nv">After</span><span class="o">=</span>multi-user.target  
 
<span class="o">[</span>Service]  
<span class="nv">User</span><span class="o">=</span>daniellet
<span class="nv">WorkingDirectory</span><span class="o">=</span>/home/daniellet
<span class="nv">ExecStart</span><span class="o">=</span>/home/daniellet/start_script.sh  
<span class="nv">Restart</span><span class="o">=</span>always  
<span class="nv">RestartSec</span><span class="o">=</span>5  
 
<span class="o">[</span>Install]  
<span class="nv">WantedBy</span><span class="o">=</span>multi-user.target 
</code></pre></div></div>

<h3 id="bash-script">Bash script</h3>

<p>Bash script to activate the python environment and run the python script. Saved as <code class="language-plaintext highlighter-rouge">start_script.sh</code> in my home directory.</p>

<div class="language-bash highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="c">#!/bin/bash</span>
<span class="nb">source env</span>/bin/activate
python3 <span class="nt">-u</span> printing_to_eink.py
</code></pre></div></div>

<h3 id="python-script-1">Python Script</h3>
<p>Here is the <code class="language-plaintext highlighter-rouge">printing_to_eink.py</code> file that is the meat of the functionality, saved in my home directory. Note the base code was originally pulled from <a href="https://learn.adafruit.com/2-13-in-e-ink-bonnet/usage">Adafruit’s usage guide</a> and then modified for my particular setup.</p>

<div class="language-python highlighter-rouge"><div class="highlight"><pre class="highlight"><code><span class="s">"""
ePaper Display text coming from a serial port using the Pillow Library.

Based on the python scripts from https://learn.adafruit.com/2-13-in-e-ink-bonnet/usage
modified for this particular use case

ASSUMES:
- connected to SSD1680Z e-ink bonnet
- screen is black and white
- there is a serial port connected on a USB
- Installed CircuitPython Blinka library from adafruit https://learn.adafruit.com/circuitpython-on-raspberrypi-linux/installing-circuitpython-on-raspberry-pi
- script is running on raspberry pi zero. Has not been tested on other raspberry pi systems (though should work...)

"""</span>

<span class="kn">import</span> <span class="nn">board</span>
<span class="kn">import</span> <span class="nn">busio</span>
<span class="kn">import</span> <span class="nn">digitalio</span>
<span class="kn">from</span> <span class="nn">PIL</span> <span class="kn">import</span> <span class="n">Image</span><span class="p">,</span> <span class="n">ImageDraw</span><span class="p">,</span> <span class="n">ImageFont</span>
<span class="kn">from</span> <span class="nn">adafruit_epd.epd</span> <span class="kn">import</span> <span class="n">Adafruit_EPD</span>
<span class="kn">import</span> <span class="nn">serial</span>
<span class="kn">import</span> <span class="nn">time</span>
<span class="kn">import</span> <span class="nn">serial.tools.list_ports</span>


<span class="kn">from</span> <span class="nn">adafruit_epd.ssd1680</span> <span class="kn">import</span> <span class="n">Adafruit_SSD1680Z</span>


<span class="c1"># First define some color constants
</span><span class="n">WHITE</span> <span class="o">=</span> <span class="p">(</span><span class="mh">0xFF</span><span class="p">,</span> <span class="mh">0xFF</span><span class="p">,</span> <span class="mh">0xFF</span><span class="p">)</span>
<span class="n">BLACK</span> <span class="o">=</span> <span class="p">(</span><span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">,</span> <span class="mh">0x00</span><span class="p">)</span>

<span class="c1"># Next define some constants to allow easy resizing of shapes and colors
</span><span class="n">BORDER</span> <span class="o">=</span> <span class="mi">5</span>
<span class="n">FONTSIZE</span> <span class="o">=</span> <span class="mi">18</span>
<span class="n">FONT_MAX_LENGTH</span> <span class="o">=</span> <span class="mi">27</span>
<span class="n">BACKGROUND_COLOR</span> <span class="o">=</span> <span class="n">WHITE</span>
<span class="n">TEXT_COLOR</span> <span class="o">=</span> <span class="n">BLACK</span>


<span class="c1"># create the spi device and pins we will need
</span><span class="n">spi</span> <span class="o">=</span> <span class="n">busio</span><span class="p">.</span><span class="n">SPI</span><span class="p">(</span><span class="n">board</span><span class="p">.</span><span class="n">SCK</span><span class="p">,</span> <span class="n">MOSI</span><span class="o">=</span><span class="n">board</span><span class="p">.</span><span class="n">MOSI</span><span class="p">,</span> <span class="n">MISO</span><span class="o">=</span><span class="n">board</span><span class="p">.</span><span class="n">MISO</span><span class="p">)</span>
<span class="n">ecs</span> <span class="o">=</span> <span class="n">digitalio</span><span class="p">.</span><span class="n">DigitalInOut</span><span class="p">(</span><span class="n">board</span><span class="p">.</span><span class="n">CE0</span><span class="p">)</span>
<span class="n">dc</span> <span class="o">=</span> <span class="n">digitalio</span><span class="p">.</span><span class="n">DigitalInOut</span><span class="p">(</span><span class="n">board</span><span class="p">.</span><span class="n">D22</span><span class="p">)</span>
<span class="n">rst</span> <span class="o">=</span> <span class="n">digitalio</span><span class="p">.</span><span class="n">DigitalInOut</span><span class="p">(</span><span class="n">board</span><span class="p">.</span><span class="n">D27</span><span class="p">)</span>
<span class="n">busy</span> <span class="o">=</span> <span class="n">digitalio</span><span class="p">.</span><span class="n">DigitalInOut</span><span class="p">(</span><span class="n">board</span><span class="p">.</span><span class="n">D17</span><span class="p">)</span>
<span class="n">srcs</span> <span class="o">=</span> <span class="bp">None</span>


<span class="c1"># e ink display
</span><span class="n">display</span> <span class="o">=</span> <span class="n">Adafruit_SSD1680Z</span><span class="p">(</span><span class="mi">122</span><span class="p">,</span> <span class="mi">250</span><span class="p">,</span> <span class="n">spi</span><span class="p">,</span> <span class="n">cs_pin</span><span class="o">=</span><span class="n">ecs</span><span class="p">,</span> <span class="n">dc_pin</span><span class="o">=</span><span class="n">dc</span><span class="p">,</span> <span class="n">sramcs_pin</span><span class="o">=</span><span class="n">srcs</span><span class="p">,</span>
                          <span class="n">rst_pin</span><span class="o">=</span><span class="n">rst</span><span class="p">,</span> <span class="n">busy_pin</span><span class="o">=</span><span class="n">busy</span><span class="p">)</span>


<span class="n">display</span><span class="p">.</span><span class="n">rotation</span> <span class="o">=</span> <span class="mi">1</span>

<span class="c1"># replace some space with a newline character so the string will fit on
# the given screen. NOTE: does not have a max number of rows check
</span><span class="k">def</span> <span class="nf">reformat_text</span><span class="p">(</span><span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">,</span> <span class="n">maxlength</span><span class="p">:</span> <span class="nb">int</span><span class="p">):</span>
    <span class="n">new_string</span> <span class="o">=</span> <span class="s">""</span>
    <span class="n">cur_row_length</span> <span class="o">=</span> <span class="mi">0</span>
    <span class="k">for</span> <span class="n">word</span> <span class="ow">in</span> <span class="n">text</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="s">' '</span><span class="p">):</span>
        <span class="k">if</span> <span class="n">cur_row_length</span> <span class="o">+</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">&lt;</span> <span class="n">maxlength</span><span class="p">:</span>
            <span class="k">if</span> <span class="nb">len</span><span class="p">(</span><span class="n">new_string</span><span class="p">)</span> <span class="o">==</span> <span class="mi">0</span><span class="p">:</span>
                <span class="n">new_string</span> <span class="o">=</span> <span class="n">word</span>
                <span class="n">cur_row_length</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="n">new_string</span> <span class="o">=</span> <span class="n">new_string</span> <span class="o">+</span> <span class="s">" "</span> <span class="o">+</span> <span class="n">word</span>
                <span class="n">cur_row_length</span> <span class="o">+=</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span> <span class="o">+</span> <span class="mi">1</span>
        <span class="k">else</span><span class="p">:</span>
            <span class="n">new_string</span> <span class="o">=</span> <span class="n">new_string</span> <span class="o">+</span> <span class="s">"</span><span class="se">\n</span><span class="s">"</span> <span class="o">+</span> <span class="n">word</span>
            <span class="n">cur_row_length</span> <span class="o">=</span> <span class="nb">len</span><span class="p">(</span><span class="n">word</span><span class="p">)</span>
    <span class="k">return</span> <span class="n">new_string</span>

<span class="c1"># re-draw the entire screen with the new text message
</span><span class="k">def</span> <span class="nf">display_text</span><span class="p">(</span> <span class="n">text</span><span class="p">:</span> <span class="nb">str</span><span class="p">):</span>
    <span class="n">image</span> <span class="o">=</span> <span class="n">Image</span><span class="p">.</span><span class="n">new</span><span class="p">(</span><span class="s">"RGB"</span><span class="p">,</span> <span class="p">(</span><span class="n">display</span><span class="p">.</span><span class="n">width</span><span class="p">,</span> <span class="n">display</span><span class="p">.</span><span class="n">height</span><span class="p">),</span> <span class="n">WHITE</span><span class="p">)</span>

    <span class="c1"># Get drawing object to draw on image.
</span>    <span class="n">draw</span> <span class="o">=</span> <span class="n">ImageDraw</span><span class="p">.</span><span class="n">Draw</span><span class="p">(</span><span class="n">image</span><span class="p">)</span>

    <span class="c1"># Draw a filled box as the background
</span>    <span class="n">draw</span><span class="p">.</span><span class="n">rectangle</span><span class="p">(</span>
        <span class="p">(</span><span class="mi">0</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="n">display</span><span class="p">.</span><span class="n">width</span> <span class="o">-</span> <span class="mi">1</span><span class="p">,</span> <span class="n">display</span><span class="p">.</span><span class="n">height</span> <span class="o">-</span> <span class="mi">1</span><span class="p">),</span>
        <span class="n">fill</span><span class="o">=</span><span class="n">BACKGROUND_COLOR</span><span class="p">,</span>
    <span class="p">)</span>

    <span class="c1"># Load a TTF Font
</span>    <span class="c1"># NOTE: linux systems normally default have this font
</span>    <span class="n">font</span> <span class="o">=</span> <span class="n">ImageFont</span><span class="p">.</span><span class="n">truetype</span><span class="p">(</span><span class="s">"/usr/share/fonts/truetype/dejavu/DejaVuSans.ttf"</span><span class="p">,</span> <span class="n">FONTSIZE</span><span class="p">)</span>

    <span class="c1"># Draw the text after its been reformated
</span>    <span class="n">new_text</span> <span class="o">=</span> <span class="n">reformat_text</span><span class="p">(</span><span class="n">text</span><span class="p">,</span> <span class="n">FONT_MAX_LENGTH</span><span class="p">)</span>
    <span class="n">draw</span><span class="p">.</span><span class="n">text</span><span class="p">(</span>
    <span class="p">(</span><span class="n">BORDER</span><span class="p">,</span> <span class="n">BORDER</span><span class="p">),</span>
    <span class="n">new_text</span><span class="p">,</span>
    <span class="n">font</span><span class="o">=</span><span class="n">font</span><span class="p">,</span>
    <span class="n">fill</span><span class="o">=</span><span class="n">TEXT_COLOR</span><span class="p">,</span>
    <span class="p">)</span>

    <span class="c1"># Display image.
</span>    <span class="n">display</span><span class="p">.</span><span class="n">image</span><span class="p">(</span><span class="n">image</span><span class="p">)</span>
    <span class="n">display</span><span class="p">.</span><span class="n">display</span><span class="p">()</span>

<span class="k">def</span> <span class="nf">process_serial_stream</span><span class="p">(</span><span class="n">ser</span><span class="p">,</span> <span class="n">line_processor</span><span class="p">):</span>
    <span class="s">"""Process continuous serial data stream"""</span>
    <span class="n">line_buffer</span> <span class="o">=</span> <span class="sa">b</span><span class="s">''</span>
    
    <span class="k">while</span> <span class="bp">True</span><span class="p">:</span>
        <span class="k">try</span><span class="p">:</span>
            <span class="c1"># Read available data
</span>            <span class="k">if</span> <span class="n">ser</span><span class="p">.</span><span class="n">in_waiting</span><span class="p">:</span>
                <span class="n">chunk</span> <span class="o">=</span> <span class="n">ser</span><span class="p">.</span><span class="n">read</span><span class="p">(</span><span class="n">ser</span><span class="p">.</span><span class="n">in_waiting</span><span class="p">)</span>
                <span class="n">line_buffer</span> <span class="o">+=</span> <span class="n">chunk</span>
                
                <span class="c1"># Process complete lines
</span>                <span class="k">while</span> <span class="sa">b</span><span class="s">'</span><span class="se">\n</span><span class="s">'</span> <span class="ow">in</span> <span class="n">line_buffer</span><span class="p">:</span>
                    <span class="n">line</span><span class="p">,</span> <span class="n">line_buffer</span> <span class="o">=</span> <span class="n">line_buffer</span><span class="p">.</span><span class="n">split</span><span class="p">(</span><span class="sa">b</span><span class="s">'</span><span class="se">\n</span><span class="s">'</span><span class="p">,</span> <span class="mi">1</span><span class="p">)</span>
                    <span class="k">try</span><span class="p">:</span>
                        <span class="n">text</span> <span class="o">=</span> <span class="n">line</span><span class="p">.</span><span class="n">decode</span><span class="p">(</span><span class="s">'utf-8'</span><span class="p">).</span><span class="n">strip</span><span class="p">()</span>
                        <span class="k">if</span> <span class="n">text</span><span class="p">:</span>
                            <span class="n">result</span> <span class="o">=</span> <span class="n">line_processor</span><span class="p">(</span><span class="n">text</span><span class="p">)</span>
                            <span class="k">if</span> <span class="n">result</span><span class="p">:</span>
                                <span class="k">yield</span> <span class="n">result</span>
                    <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
                        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Process error: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
            <span class="k">else</span><span class="p">:</span>
                <span class="n">time</span><span class="p">.</span><span class="n">sleep</span><span class="p">(</span><span class="mf">0.001</span><span class="p">)</span>
                
        <span class="k">except</span> <span class="nb">KeyboardInterrupt</span><span class="p">:</span>
            <span class="k">break</span>
        <span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
            <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Stream error: </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>

<span class="k">def</span> <span class="nf">parse_msg</span><span class="p">(</span><span class="n">line</span><span class="p">):</span>
    <span class="s">"""check if it was a detection, and return if so"""</span>
    <span class="c1"># for testing just return it all
</span>    <span class="c1"># TODO can add filtering here if the line is something we dont want to print
</span>    <span class="c1"># e.g. only print lines that say "DEBUG" or "ERR" in them
</span>    <span class="k">return</span> <span class="n">line</span>

<span class="k">print</span><span class="p">(</span><span class="s">"Booting up..."</span><span class="p">)</span>
<span class="n">display_text</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">time</span><span class="p">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"[%H</span><span class="si">:</span><span class="o">%</span><span class="n">M</span><span class="si">:</span><span class="o">%</span><span class="n">S</span><span class="p">]</span><span class="s">")</span><span class="si">}</span><span class="s"> Booting up..."</span><span class="p">)</span>
<span class="n">last_display_update</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
<span class="c1"># trigger serial tracking
</span><span class="n">last_serial_string</span> <span class="o">=</span> <span class="s">""</span>
<span class="k">try</span><span class="p">:</span>

    <span class="c1">#find espressif connected board
</span>    <span class="c1">#TODO if connected dev board is diff, need to update the PID/VID here
</span>    <span class="n">portname</span> <span class="o">=</span> <span class="s">""</span>
    <span class="k">for</span> <span class="n">p</span> <span class="ow">in</span> <span class="n">serial</span><span class="p">.</span><span class="n">tools</span><span class="p">.</span><span class="n">list_ports</span><span class="p">.</span><span class="n">comports</span><span class="p">():</span>
        <span class="k">if</span> <span class="n">p</span><span class="p">.</span><span class="n">pid</span> <span class="o">==</span> <span class="mi">4097</span> <span class="ow">and</span> <span class="n">p</span><span class="p">.</span><span class="n">vid</span> <span class="o">==</span> <span class="mi">12346</span><span class="p">:</span>
            <span class="n">portname</span> <span class="o">=</span> <span class="n">p</span><span class="p">.</span><span class="n">device</span>

    <span class="k">if</span> <span class="n">portname</span> <span class="o">==</span> <span class="s">""</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="s">"CANT FIND PORT"</span><span class="p">)</span>
    <span class="k">else</span><span class="p">:</span>
        <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Found port </span><span class="si">{</span><span class="n">portname</span><span class="si">}</span><span class="s">, connecting..."</span><span class="p">)</span>
        <span class="c1"># assumes connection is 115200 baud rate
</span>        <span class="c1"># TODO update baudrate if needed
</span>        <span class="k">with</span> <span class="n">serial</span><span class="p">.</span><span class="n">Serial</span><span class="p">(</span><span class="n">portname</span><span class="p">,</span> <span class="mi">115200</span><span class="p">,</span> <span class="n">timeout</span><span class="o">=</span><span class="mi">1</span><span class="p">)</span> <span class="k">as</span> <span class="n">ser</span><span class="p">:</span>
            <span class="k">for</span> <span class="n">detection</span> <span class="ow">in</span> <span class="n">process_serial_stream</span><span class="p">(</span><span class="n">ser</span><span class="p">,</span> <span class="n">parse_msg</span><span class="p">):</span>
                <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"detected string: </span><span class="si">{</span><span class="n">detection</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
                <span class="c1"># print all new messages, and if it's been the same message for over 60 seconds
</span>                <span class="c1"># (e.g. the same status message of "scanning" or something)
</span>                <span class="c1"># print the same message but with updated timestamp, to show its not dead
</span>                <span class="k">if</span> <span class="p">(</span><span class="n">last_serial_string</span> <span class="o">!=</span> <span class="n">detection</span> <span class="ow">or</span> <span class="p">(</span><span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span> <span class="o">-</span> <span class="n">last_display_update</span><span class="p">)</span> <span class="o">&gt;</span> <span class="mi">60</span><span class="p">):</span>
                    <span class="n">display_text</span><span class="p">(</span><span class="sa">f</span><span class="s">"</span><span class="si">{</span><span class="n">time</span><span class="p">.</span><span class="n">strftime</span><span class="p">(</span><span class="s">"[%H</span><span class="si">:</span><span class="o">%</span><span class="n">M</span><span class="si">:</span><span class="o">%</span><span class="n">S</span><span class="p">]</span><span class="s">")</span><span class="si">}</span><span class="s"> </span><span class="si">{</span><span class="n">detection</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
                    <span class="n">last_serial_string</span> <span class="o">=</span> <span class="n">detection</span>
                    <span class="n">last_display_update</span> <span class="o">=</span> <span class="n">time</span><span class="p">.</span><span class="n">time</span><span class="p">()</span>
                    
<span class="k">except</span> <span class="nb">Exception</span> <span class="k">as</span> <span class="n">e</span><span class="p">:</span>
    <span class="k">print</span><span class="p">(</span><span class="sa">f</span><span class="s">"Serial connection failed </span><span class="si">{</span><span class="n">e</span><span class="si">}</span><span class="s">"</span><span class="p">)</span>
</code></pre></div></div>]]></content><author><name></name></author><category term="technical write up" /><category term="side project" /><category term="How-to" /><category term="side project" /><category term="embedded" /><summary type="html"><![CDATA[Playing around with different development projects, there’s often a need to listen on a USB serial port connection to see log outputs. If I’m making a device that I want to test out in the wild, without a full display yet, it’d be nice to have a grab-and-go tool for that. So, I decided to put together a raspberry pi zero I had lying around, plus the adafruit e-ink bonnet and a powerbank, to let me read serial logs on the road.]]></summary></entry><entry><title type="html">Tracking Aircraft with a Software Defined Radio</title><link href="/blog/2026/3/3/adsb-radio-setup" rel="alternate" type="text/html" title="Tracking Aircraft with a Software Defined Radio" /><published>2026-03-03T00:00:00+00:00</published><updated>2026-03-03T00:00:00+00:00</updated><id>/blog/2026/3/3/adsb-radio-setup</id><content type="html" xml:base="/blog/2026/3/3/adsb-radio-setup"><![CDATA[<ul id="markdown-toc">
  <li><a href="#basics-for-flight-tracking" id="markdown-toc-basics-for-flight-tracking">Basics for Flight Tracking</a></li>
  <li><a href="#slightly-more-in-depth-info-for-flight-tracking" id="markdown-toc-slightly-more-in-depth-info-for-flight-tracking">Slightly more in-depth info for Flight Tracking</a></li>
  <li><a href="#help-my-eyes-just-glazed-over" id="markdown-toc-help-my-eyes-just-glazed-over">Help, My Eyes Just Glazed Over!</a></li>
  <li><a href="#why-can-i-see-a-helicopter-out-my-window-but-not-online" id="markdown-toc-why-can-i-see-a-helicopter-out-my-window-but-not-online">Why can I see a helicopter out my window but not online?</a></li>
  <li><a href="#the-hardware-and-setup" id="markdown-toc-the-hardware-and-setup">The Hardware and Setup</a>    <ul>
      <li><a href="#initial-setup" id="markdown-toc-initial-setup">Initial setup</a></li>
      <li><a href="#how-well-it-works" id="markdown-toc-how-well-it-works">How well it works</a></li>
      <li><a href="#fun-configurations" id="markdown-toc-fun-configurations">Fun configurations</a></li>
    </ul>
  </li>
  <li><a href="#interesting-planes" id="markdown-toc-interesting-planes">Interesting Planes</a>    <ul>
      <li><a href="#short-skyvan" id="markdown-toc-short-skyvan">Short SkyVan</a></li>
      <li><a href="#747-dreamlifter" id="markdown-toc-747-dreamlifter">747 Dreamlifter</a></li>
    </ul>
  </li>
  <li><a href="#next-steps" id="markdown-toc-next-steps">Next steps</a></li>
  <li><a href="#resources" id="markdown-toc-resources">Resources</a></li>
</ul>

<p>Radio has always been a side interest of mine, but seemed too convoluted for me to easily get into. However, now with Software-Defined Radios (SDR), things started to shift into the software world, so I started to get my toes wet. One use that seemed pretty popular was tracking airplanes, so I thought I’d give it a go.</p>

<p>I had previously setup an ADSB receiver using the <a href="https://www.adsbexchange.com/how-to-feed/adsbx-custom-pi-image/">adsb exchange raspberry pi image</a>, and found it a little finicky. I also had other uses for the pi, so I ended up retiring that setup, with the eventual plan to get it back up and running “sometime in the future”. Which turns out is around now! (don’t…don’t ask how long it was since I did that setup the first time)</p>

<p>This time, I came across a different raspberry pi image that was actually default included in the <a href="https://www.raspberrypi.com/software/">rasperry pi imager tool</a>, called <a href="https://adsb.im/home">ADSB.im</a>, and decided to give it a try again. I’ve had it setup for about a month now (probably more once this is published!), and have learned a lot, so I thought I’d share.</p>

<p><img src="/assets/adsb-radio-setup/tracking-screenshot.png" alt="screenshot of a map with aircraft, their route, and callsigns displayed in their actual location" /></p>

<!--more-->

<h2 id="basics-for-flight-tracking">Basics for Flight Tracking</h2>

<p>Besides looking at planes with your eyeballs, there’s a variety of ways to track aircraft. The most common is looking at the ADSB data. The <a href="https://en.wikipedia.org/wiki/Automatic_Dependent_Surveillance%E2%80%93Broadcast">Automatic Dependent Surveillance–Broadcast</a> is broadcast on the 1090 Mhz or 978 MHz frequency, sending out info like speed, course, altitude, callsign and identification of an aircraft. You don’t even have to setup your own station to see this, you can go to <a href="https://globe.adsbexchange.com/">ADSBExchange</a> or <a href="https://adsb.lol/">ADSB.lol</a> to see them right now!</p>

<p>There’s also a lot of ways to listen in on the air traffic control conversations, but that’s not talked about in this article because that’s a different can of worms. Though I do want to mention, the Minneapolis airport has a live youtube video that shows the airport and also plays the conversations: <a href="https://www.youtube.com/live/FY6WlMjzG2U?si=9Io45ISSSH3IGIuM">https://www.youtube.com/live/FY6WlMjzG2U?si=9Io45ISSSH3IGIuM</a></p>

<h2 id="slightly-more-in-depth-info-for-flight-tracking">Slightly more in-depth info for Flight Tracking</h2>

<p>To get more in-depth, sure hope you like some acronyms! The two main frequency we care about right now is the 978MHz and the 1090MHz.</p>

<p><strong>978MHz</strong> - The 978MHz frequency is used by the universal access transceiver (UAT). UAT is intended to support not only ADS-B, but also flight information service – broadcast (FIS-B), traffic information service – broadcast (TIS-B),</p>

<p><strong>FIS-B</strong> - is only broadcast on UAT, and provides weather info to aircraft</p>

<p><strong>TIS-B</strong> - is broadcast on both 1090MHz <em>and</em> 978MHz, and provides info from ground stations about aircraft that only have a basic transponder and aren’t transmitting out ADSB data directly.</p>

<p><strong>1090MHz</strong> has both Mode S and 1090 extended squitter (ES) on it. The Mode S was a replacement for a different protocol, air traffic control radar beacon system (ATCRBS). And the 1090 ES is indeed an extension of the protocol, with all the ADSB data.</p>

<p>If you want to play with even more frequencies:</p>

<p><strong>ACARS</strong> - Aircraft Communications Addressing and Reporting System. Standard ACARS transmits at a frequency of 131.550 MHz. This is used for communicating between air traffic control and the aircraft.</p>

<p><strong>VDL2</strong> - As <a href="https://www.rtl-sdr.com/receiving-vdl-mode-2-multipsk-rtl-sdr/">this article</a> states: The VHF Data Link mode 2 (VDL2) is a new transmission mode used on aircraft for sending short messages, position data (similar to ADS-B) and also for allowing traffic controllers to communicate to pilots via text and data. VDL2 is intended to eventually replace the standard ACARS modes. It is found at 136.975 MHz.</p>

<h2 id="help-my-eyes-just-glazed-over">Help, My Eyes Just Glazed Over!</h2>

<p>ADSB is messages sent out by planes and helicopters, it gives info about where they are and where they’re heading. You can watch them in real time online, or setup a local station with a software defined radio, a raspberry pi, and some real simple software to glue it all together!</p>

<h2 id="why-can-i-see-a-helicopter-out-my-window-but-not-online">Why can I see a helicopter out my window but not online?</h2>
<p>When I first started learning about this, there were a couple times I spotted a plane or helicopter, but when I went to the adsb tracker, I didn’t see anything! Turns out, there’s a couple reasons a plane or helicopter might not be seen in an online ADSB aggregator websites.</p>

<ul>
  <li>
    <p>The LADD flag - <a href="https://www.faa.gov/pilots/ladd">LADD Program</a> was created by the FAA which lets private aircraft owers to request their aircraft’s flight data be removed from general distribution. Some ADSB websites follow this flag and remove the aircraft, some do not. As of this writing, I know <a href="https://adsb.lol/">adsb.lol</a> still displays them.</p>
  </li>
  <li>
    <p>Their transponder is off - Some government agencies (e.g. DHS) are allowed to turn off their ADSB transponders when they’re in flight on various missions. This means the ADSB data simply doesn’t exist to be collected! So they can’t be tracked in this way.</p>
  </li>
  <li>
    <p>Poor coverage - The ADSB aggregator websites are, in fact, aggregators of data, normally simply volunteers uploading (creating “feeders” like described below). If there’s no one near enough monitoring and feeding the ADSB data to aggregators, it wont show up. That also means you’re in the perfect place to help out the global aviation community!</p>
  </li>
</ul>

<h2 id="the-hardware-and-setup">The Hardware and Setup</h2>

<p>Hardware needed:</p>
<ul>
  <li><a href="https://www.adafruit.com/product/4295">Raspberry Pi 4 model B with 1GB RAM</a></li>
  <li><a href="https://www.adafruit.com/product/1497">Software Defined Radio from adafruit</a></li>
  <li><a href="https://www.adafruit.com/product/4298">raspberry pi power source</a></li>
  <li>SD card</li>
</ul>

<p>With everything all connected, it’s a pretty compact setup I can put on my second floor windowsill. Since the airport is south of me, I put it in my south-facing window.</p>

<p><img src="/assets/adsb-radio-setup/sdr-adsb-setup.jpg" alt="Picture showing the raspberry pi connected to an SDR USB which connects to a small black antenna sitting next to it, on a white windowsill" /></p>

<h3 id="initial-setup">Initial setup</h3>
<p>I found the <a href="https://adsb.im">ADSB.im</a> website had a really good <a href="https://adsb.im/howto">setup guide</a> that was straight-forward to follow. The biggest issue was simply trying to find the IP address on my network once the pi was booted up. For some reason the various auto-finder options weren’t working, so I had to dredge up some old networking knowledge.</p>

<p>To hunt for the pi on my network, I ran <code class="language-plaintext highlighter-rouge">ifconfig</code> on my linux laptop to see what my IP address was, then did a quick ping scan with <code class="language-plaintext highlighter-rouge">nmap</code> to see who else was on my network (I knew no one else but me was connected right now)</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>nmap -sn 192.168.68.0/24
</code></pre></div></div>
<p>That quickly showed where it was living, and I went to port <code class="language-plaintext highlighter-rouge">80</code> in my browser, and success!</p>

<h3 id="how-well-it-works">How well it works</h3>

<p>Even with the default antenna and no fine-tuning on my part, I’m getting aircraft! The pi image comes with graphs1090 data, so I can even see some more in-depth data on my system. My median range is about 11.7 nautical miles, which is about 13.4 regular ole miles. This feels pretty decent to me, and since I’m so close to the minneapolis airport, I still get a lot of aircraft even with that distance</p>

<p><img src="/assets/adsb-radio-setup/adsb_range.png" alt="screenshot of graphs1090 range graph" /></p>

<p>On the pi side, I can see the tracking is very low CPU usage, which is nice, I’m not burning up the whole pi trying to do anything too fancy.</p>

<p><img src="/assets/adsb-radio-setup/cpu_utilization.png" alt="screenshot of graphs1090 cpu utilization" /></p>

<p>I also enabled skystats (more below), which increases IO writes, and boy can you see that in the disk IO graph. I assume at some point that will fry the SD card in my pi, but for now, I’m fine with being rough on it in exchange for fun info about planes.</p>

<p><img src="/assets/adsb-radio-setup/disk_io.png" alt="screenshot of graphs1090 disk IO graph" /></p>

<h3 id="fun-configurations">Fun configurations</h3>

<p>In the Setup -&gt; Advanced setting on the website, you can enable SkyStats</p>

<p><img src="/assets/adsb-radio-setup/Advanced-setup-skystats.png" alt="Screenshot of the Advanced settings page" /></p>

<p><a href="https://github.com/tomcarman/skystats">Skystats</a> is a docker container you can run on the raspberry pi that watches the ADSB data coming in and collects various stats, and of particular interest, flags interesting aircraft using a local copy of <a href="https://github.com/sdr-enthusiasts/plane-alert-db">plane-alert-db</a>, which lists interesting aircraft and why they’re interesting. It also hooks into various airplane photo websites, like <a href="https://www.jetphotos.com/">jetphotos.com</a> to give you pictures, even if you missed seeing it yourself as it flew by.</p>

<p>There’s a lot of other configurations I haven’t even touched on the pi, but that’s a future todo for now.</p>

<h2 id="interesting-planes">Interesting Planes</h2>

<p>I’ve never really noticed planes outside of the constant stream of Delta flights heading out from the airport, so it was fun to see models I’d never heard of before. Here’s a couple that stuck out.</p>

<h3 id="short-skyvan">Short SkyVan</h3>

<p>I’ve never heard of this plane before, but when I was poking around at SkyStat’s interesting airplanes, this just made me laugh. Look at this ridiculous plane!</p>

<p><img src="/assets/adsb-radio-setup/win_aviation.png" alt="Picture of the Short SC.7 Skyvan airplane. It is light grey, with a snub noise and a very square-looking body" />
(Image taken from jetphotos.com)</p>

<p>Looking into who owns it and what it’s for, it’s apparently for training paratroopers and owned by a defense company to help military training. So, maybe not so funny. But it still looks ridiculous. It flew over Minneapolis back on february 8th</p>

<p><a href="https://adsb.lol/?icao=a18435&amp;lat=45.221&amp;lon=-93.803&amp;zoom=6.2&amp;showTrace=2026-02-08">Route on adsb.lol</a></p>

<h3 id="747-dreamlifter">747 Dreamlifter</h3>

<p>Skystats tags this as “Absolute Unit”, and you know what, it sure is.</p>

<p><img src="https://cdn.jetphotos.com/full/5/63687_1630935322.jpg" alt="Side view of a boeing 747. However, the top of the main area that normally has passengers bulges upwards, making the body of the aircraft about twice as high" /></p>

<p>The Boeing 747 dreamlifter was designed to be able to ship the Boeing <em>787</em> parts between Italy, Japan, and the US. And it appears that’s exactly what it was doing, flying from Japan, to anchorage, down to Charlston, flying over Minneapolis en route.</p>

<p><a href="https://adsb.lol/?icao=aa90a0&amp;lat=42.598&amp;lon=-93.155&amp;zoom=5.5&amp;showTrace=2026-02-04">Route on adsb.lol</a></p>

<h2 id="next-steps">Next steps</h2>

<p>Some next steps I want to do:</p>
<ul>
  <li>Learn more about antenna selection. That apparently can make a huge difference in range, but I just have the stock antenna for my SDR.</li>
  <li>Hook into other sources - ACARS and VDL2 could be interesting to listen in on, and it’s apparently possible with an SDR. Maybe I should get a second one to complement my ADSB listening?</li>
</ul>

<h2 id="resources">Resources</h2>

<p>Build your own feeder:</p>
<ul>
  <li><a href="https://adsb.im/home">ADSB.im raspberry pi image</a></li>
</ul>

<p>Watch planes online:</p>
<ul>
  <li><a href="https://globe.adsbexchange.com/">ADSBExchange</a></li>
  <li><a href="https://adsb.lol/">ADSB.lol</a></li>
</ul>

<p>Some interesting other things to do while tracking aircraft:</p>
<ul>
  <li><a href="https://www.rtl-sdr.com/rtl-sdr-radio-scanner-tutorial-receiving-airplane-data-with-acars/">Receiving Airplane data with ACARS</a></li>
  <li><a href="https://www.rtl-sdr.com/receiving-vdl-mode-2-multipsk-rtl-sdr/">Receiving VDL mode 2</a></li>
</ul>

<p>Fun plane data:</p>
<ul>
  <li><a href="https://github.com/tomcarman/skystats">Skystats</a></li>
  <li><a href="https://github.com/sdr-enthusiasts/plane-alert-db">plane-alert-db</a></li>
</ul>]]></content><author><name></name></author><category term="technical write up" /><category term="side project" /><category term="How-to" /><category term="SDR" /><category term="side project" /><category term="embedded" /><summary type="html"><![CDATA[Radio has always been a side interest of mine, but seemed too convoluted for me to easily get into. However, now with Software-Defined Radios (SDR), things started to shift into the software world, so I started to get my toes wet. One use that seemed pretty popular was tracking airplanes, so I thought I’d give it a go. I had previously setup an ADSB receiver using the adsb exchange raspberry pi image, and found it a little finicky. I also had other uses for the pi, so I ended up retiring that setup, with the eventual plan to get it back up and running “sometime in the future”. Which turns out is around now! (don’t…don’t ask how long it was since I did that setup the first time) This time, I came across a different raspberry pi image that was actually default included in the rasperry pi imager tool, called ADSB.im, and decided to give it a try again. I’ve had it setup for about a month now (probably more once this is published!), and have learned a lot, so I thought I’d share.]]></summary></entry><entry><title type="html">What I’ve been up to lately</title><link href="/blog/2026/2/4/Latest-articles" rel="alternate" type="text/html" title="What I’ve been up to lately" /><published>2026-02-04T00:00:00+00:00</published><updated>2026-02-04T00:00:00+00:00</updated><id>/blog/2026/2/4/Latest%20articles</id><content type="html" xml:base="/blog/2026/2/4/Latest-articles"><![CDATA[<p>Well look here, it’s been a bit. Turns out full time jobs keep you busy. Just a short post to share some things I’ve been doing and writings I’ve done elsewhere.</p>

<!--more-->

<p>I did end up transitioning to embedded firmware development, first at Medtronic for 3 years, and then another year at Dojo Five, a small local consultancy company. I’ve gone to <a href="https://www.crowdsupply.com/teardown/portland-2026">Teardown</a>, a great conference that I hope to go to again this year. I’ve also had some time volunteering with the local fixit clinics, fixing small household appliances and meeting some cool people there.</p>

<p>I have some plans for more articles around using an oscilliscope, circuit analysis for FW engineers, getting into ADSB flight tracking, Lora and meshtastic, and more.</p>

<p>While you wait, here’s a couple Dojo Five Blog posts I wrote while I worked there:</p>

<p><a href="https://dojofive.com/blog/test-equipment-automation/">An Intro to Test Equipment Automation with VISA, SCPI, and the power of Python</a></p>

<p><a href="https://dojofive.com/blog/nanopb-traps-and-how-to-avoid-them/">An Overview of the quirks of NanoPB</a></p>

<p><a href="https://dojofive.com/blog/transitioning-from-bare-metal-nordic-sdk-build-system/">The Basics of going from bare metal to using the Nordic SDK Build system</a></p>]]></content><author><name></name></author><summary type="html"><![CDATA[Well look here, it’s been a bit. Turns out full time jobs keep you busy. Just a short post to share some things I’ve been doing and writings I’ve done elsewhere.]]></summary></entry><entry><title type="html">Bluetooth Low Energy and Reversing a Smart Lightbulb</title><link href="/blog/2021/9/22/using-adafruits-bluetooth-sniffer" rel="alternate" type="text/html" title="Bluetooth Low Energy and Reversing a Smart Lightbulb" /><published>2021-09-22T00:00:00+00:00</published><updated>2021-09-22T00:00:00+00:00</updated><id>/blog/2021/9/22/using-adafruits-bluetooth-sniffer</id><content type="html" xml:base="/blog/2021/9/22/using-adafruits-bluetooth-sniffer"><![CDATA[<p>“Smart” things are the wave of the future! Everyone wants some bit of tech that already exists, like a lightbulb, but now controlled through an app! Or at least, that’s what marketers have decided on. But sometimes the companies maintaining the app goes under, or decides to not support what you bought anymore, and your fancy smart lightbulb becomes, well, a $35 lightbulb.</p>

<p>I recently got my hands on an old <a href="https://www.amazon.com/gp/product/B00Y6X93EQ/ref=ppx_yo_dt_b_asin_title_o05_s00?ie=UTF8&amp;psc=1">bluetooth lightbulb</a> that was first made in 2015. But the phone app to control it is clunky and ugly, and hasn’t been updated since 2018. So if I want to still use it now, the options are to have an unmaintaned, potential security-risk app on my phone, or not use it. <em>Or</em>, be me, since I’ve been itching to learn more about bluetooth low energy. In which case, it becomes time to do some reverse engineering!</p>

<p><img src="/assets/bluetooth-sniffing/excerpt_image.png" alt="Wireshark and phone app next to each other" /></p>

<!--more-->

<h2 id="the-plan">The Plan</h2>
<p>Use the <a href="https://www.adafruit.com/product/2269">adafruit bluetooth LE sniffer</a> to watch communication between the app and the lightbulb, and see if I can figure out how to turn it on/off, change the color, and set timers.</p>

<h2 id="intro-to-bluetooth-low-energy">Intro to Bluetooth Low Energy</h2>

<p>First, the bluetooth light will advertise itself using Generic Access Profile, aka GAP, until an app or what have you connects to it. I’m not going to go into that right now, because for reverse engineering, I know it connects, I want to know what it does once it’s connected. That means I need to focus on a different protocol, Generic Attribute Profile, aka GATT.</p>

<p>Once a bluetooth low energy thing is connected to the app that controls it, it switches to using GATT to communicate. GATT is what allows you to interact with the lightbulb (or other peripheral device). You can read data from the lightbulb (e.g. what color it is right now), or send data (e.g. setting the color, setting the timer).</p>

<p>For GATT there are three concepts that are the most important: profiles, services, and characteristics. Profiles are collections of services, and services have a collection of characteristics. Characteristics are basically just key/value pairs that let you read and write data to the connected device. Services collect related characteristics (e.g. color control characteristics would all be grouped in a color service). Profiles are an abstract collection of services, not on a device itself. Services and characteristics ARE on the device.</p>

<p>Let’s do a made-up example of those three concepts. Say I want to make a Lightbulb Profile. Profiles are conceptual collections of services. For this lighbulb profile, I want to have two different services. One service is the Light service, which will control the light color and on/off state. The other service is the timer service. That’ll control what timers a user sets (e.g. have the light turn on at 9am). The Light service will probably need to read and write the current hex color of the lightbulb, so it’ll need a characteristic with a value of the current hex color, and permissions to both read and write. It’ll also need a second characteristic, maybe just called the power characteristic, and the value is 1 or 0, depending on if it’s on or off. I’d also set that to be both read and write, so I could see if the light is turned on, and if not, set it to on.</p>

<p>The Timer service will also have a collection of characteristics that will let it view current timers on the device, add/delete timers, etc.</p>

<p>Here’s a diagram of the above example, to give you another view. Note if you connected to the lightbulb, you wouldn’t see the profile info, just the services and characteristics.</p>

<p><img src="/assets/bluetooth-sniffing/bluetooth_LightProfile_Example.png" alt="Drawn diagram of above example" /></p>

<blockquote>
  <p><strong>NOTE</strong> this is not how the actual lightbulb I’m using is setup, this is just an example of a way to structure a bluetooth LE device to get the concepts in your head</p>
</blockquote>

<p>There’s one final bit that is used a lot: the characteristic descriptor. This is attached to a characteristic, and is basically metadata about the characteristic. So for the earlier Lightbulb profile, I can create a characteristic descriptor that is attached to the Color characteristic, and set the descriptor to have a value of “the color of the light in hex”. This helps me if I’m troubleshooting an app to talk with the lightbulb, or when I’m reverse engineering.</p>

<p>The conceptual model I currently have is something like this:</p>

<p><img src="/assets/bluetooth-sniffing/bluetooth_UML_Style_Explanation.png" alt="Simple diagram with boxes connected with lines. Profiles, services, characteristics, and characteristic descriptors are each a seperate box with a line between each" /></p>

<p>The little “+” on the line indicates that the box above it can have one or more. So a profile can have one or more services in it. The “*” means it can have zero or one. So the Characteristics can have zero or more characteristic descriptors. The words tabbed in are individual fields. So Characteristics have permissions and a value.</p>

<p>However, that’s the conceptual idea, not the implementation. In a more in-depth way, GATT is a big table. Each row has a handle to reference it, kind of like a key in a key/value pair. Then there’s a type, which is a UUID that either matches up with the <a href="https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf">16-bit UUID Numbers Document</a> or is custom. Then permissions (READ, WRITE, NOTIFY), and lastly, some sort of value, which depends on the type of that row. Yes, that’s pretty of confusing.</p>

<blockquote>
  <p><strong>NOTE</strong> That <a href="https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf">16-bit UUID Numbers Document</a> is really important. It lists all bluetooth-defined UUIDs. These UUIDs are used all over the place to identify the type of row each row in the GATT table is, types of characteristics and descriptors, etc.</p>
</blockquote>

<p>Remember that conceptual model? That maps to this table, I’ll walk you through making your own GATT table. Since the profiles are abstract, we can just ignore them for now. So first, we have to declare we have a service. We do this by creating a handle (in my example below, 0x001), setting the type as “Service Declaration”, permission to read only, and then the value as a UUID that says what sort of service it is. Bluetooth has a lot of declared “GATT Service Type” UUIDs in the document I linked above, you can browse them if you want.</p>

<p>Now that a service has been declared, lets declare some characteristics. Instead of having a single row where the “value” column is the value of the characteristic, we first have to do a Characteristic Declaration, like we did the service. So that gets its own handle, the type is “Characteristic Declaration”, it’s read only, and the value has several bits of info:</p>

<ul>
  <li>the handle of the characteristic value (I’ll get to that next)</li>
  <li>the type of characteristic it is</li>
  <li>the permissions for this characteristic (e.g. if it’s WRITE or READ only).</li>
</ul>

<p>The type of characteristic is one of the “GATT Characteristic and Object Type” UUIDs in the bluetooth doc linked above, <em>OR</em> a custom UUID that the devs decide on.</p>

<p>Once we have have the Characteristic <em>declaration</em> we can add a row talking about the Characteristic’s <em>value</em>. It gets a handle, type, and permission that all have to match what the Characteristic Declaration said it would have. Finally, the value field for this row is the actual value set by the peripheral. So if this characteristic lets me read the hex color the light is currently set to, the value would be some hex value like <code class="language-plaintext highlighter-rouge">64ff32</code>.</p>

<p>And lastly, Characteristic Descriptors! These don’t get a declaration and then a value row like Characteristics, instead they’re just a single row after the Characteristic Value row. They get a handle, the type is one of the “GATT Descriptor” UUIDs from the bluetooth doc, or a custom one set by a dev, permissions (what permissions depends on the type), and lastly a value. A common descriptor is the “Characteristic User Description Descriptor”, which is UUID 0x2901. The value for this descriptor is a user-readable description for the given descriptor. So for the example characteristic above, a characteristic description descriptor would have a value of “Hex value of light” or something.</p>

<p>Here’s the table I described above, but in actual table-form:</p>

<table>
  <thead>
    <tr>
      <th>Handle</th>
      <th>Type (defined by UUID)</th>
      <th>Permissions</th>
      <th>Value</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0x001</td>
      <td>Service declaration UUID (0x2800 for primary services, 0x2801 for secondary)</td>
      <td>read only</td>
      <td>UUID that matches the “GATT Service type” UUIDs from Bluetooth, or custom value</td>
    </tr>
    <tr>
      <td>0x002</td>
      <td>Characteristic Declaration UUID (0x2803)</td>
      <td>read only</td>
      <td>This characteristic value’s: handle (in this case 0x003), type (one of the “GATT Characteristic and Object Type” UUIDs from bluetooth, or custom), permissions</td>
    </tr>
    <tr>
      <td>0x003</td>
      <td>type declared by 0x002</td>
      <td>permissions declared by 0x002</td>
      <td>some value set by the peripheral</td>
    </tr>
    <tr>
      <td>0x004</td>
      <td>Characteristic Descriptor (one of the “GATT Descriptor” UUIDs from bluetooth or custom)</td>
      <td>depends on descriptor type</td>
      <td>some sort of value, e.g. “Hex value of light”</td>
    </tr>
  </tbody>
</table>

<p>Oof that’s a lot of theory and examples, let’s get back to what I wanted to do in the first place, poke around at a lightbulb.</p>

<h2 id="exploring-the-lightbulb">Exploring the Lightbulb</h2>
<p>Now that I have some idea what I’m looking at, let’s start looking at the lightbulb. There’s several apps that help troubleshoot and look at bluetooth LE connections, I used the <a href="https://play.google.com/store/apps/details?id=no.nordicsemi.android.mcp&amp;hl=en">nordic smartphone app for android</a>. That gave me some basic lightbulb info:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Name: LEDBlue-8B109BEB 
Address: 50:33:8B:10:9B:EB
RSSI: -46
Last advertisement packet:
Raw data: 0x0201060702F0FFE5FFE0FF12094C4544426C75652D384231303942454220051210001400020A04
</code></pre></div></div>
<p>The Address info is important because that is how it’ll appear in wireshark! Let’s explore some of the services and characteristics the lightbulb is advertising, to give some hints for when we start sniffing.</p>

<p><img src="/assets/bluetooth-sniffing/Screenshot_nordicApp_ServicesList.png" alt="Screenshot of the nordic app displaying list of services for the lightbulb" /></p>

<p>It has several standard services that are designed by bluetooth, the Generic Access, Generic Attribute and Device Information Service. Poking around at them, though, it looks like the characteristics in those services are mostly set to default values, or not set at all. It also has the following Unknown UUID services:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>0000fff0-0000-1000-8000-00805f9b34fb
0000ffe5-0000-1000-8000-00805f9b34fb
0000ffe0-0000-1000-8000-00805f9b34fb
</code></pre></div></div>
<p>The format of the UUIDs follow the bluetooth standard (0000XXXX-0000-1000-8000-00805f9b34fb where XXXX is a unique value), but I looked up the services on bluetooth’s <a href="https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf">16-bit UUID Numbers Document</a> and didn’t find anything. So presumably these are custom services designed by the company that made the lightbulb.</p>

<p>I poked around in the different services, and the 0xFFE5 service seems the most promising for controlling light colors. It has several characteristics with descripters that say “Green” or “Red” or “RGBW 4 bytes”. The individual colors have <code class="language-plaintext highlighter-rouge">READ, WRITE</code> permissions, but the RGBW characteristic is <code class="language-plaintext highlighter-rouge">WRITE NO RESPONSE</code>, which is interesting to note.</p>

<p><img src="/assets/bluetooth-sniffing/Screenshot_nordicApp_CharacteristicDescriptionOfColors.png" alt="Screenshot of nordic app displaying some of the characteristics of the 0xFFE5 service. The Characteristic User Descriptions have value of &quot;Blue&quot; or &quot;Green&quot; or &quot;RGBW 4 bytes&quot;" /></p>

<p>The other unknown services are not has helpful, and most of the characteristics have generic “Characteristic 2” descriptors attached to them. Apparently the devs got tired of giving useful descriptions at this point?</p>

<p><img src="/assets/bluetooth-sniffing/Screenshot_nordicApp_CharDescGenericDesc.png" alt="Screenshot of nordic app displaying characteristics with descriptions like &quot;Characteristic 1&quot; or &quot;Characteristic 2&quot;" /></p>

<p>I think this is about the limit of interesting things I can do just looking at the nordic app. Let’s switch to sniffing!</p>

<h2 id="setting-up-the-sniffer">Setting up the Sniffer</h2>

<p>Once again, Adafruit has a <a href="https://learn.adafruit.com/introducing-the-adafruit-bluefruit-le-sniffer/using-with-sniffer-v2">nice writeup</a> on the setup for the bluetooth sniffer.</p>

<p>The basic steps I did was:</p>
<ol>
  <li>downloaded extcap folder from adafruit</li>
  <li>copied into my extcap folder for wireshark, which for me was at: /usr/lib/x86_64-linux-gnu/wireshark/extcap</li>
  <li>ran <code class="language-plaintext highlighter-rouge">python --version</code> to make sure it was 2.7</li>
  <li>tested by going to the extcap directory in terminal and running <code class="language-plaintext highlighter-rouge">python nrf_sniffer.py</code></li>
  <li>ran <code class="language-plaintext highlighter-rouge">sudo pip2 install pyserial</code> to get rid of serial error</li>
  <li>tested again and got “No arguments given!” which indicates it’s running right</li>
</ol>

<p>Once the initial setup was done, I closed wireshark, plugged in the sniffer, re-opened wireshark, and found the sniffer listed as “nrf-Sniffer” in the list of local interfaces on wireshark. But when I double clicked to run, I got the error: “Couldn’t run /usr/bin/dumpcap in child process: Permission denied”. Turns out my account wasn’t included in the <code class="language-plaintext highlighter-rouge">wireshark</code> group, so I had to update it by running <code class="language-plaintext highlighter-rouge">sudo usermod -a -G wireshark danielle</code>. Once I logged off and then logged on again it worked. I was sniffing packets!</p>

<blockquote>
  <p><strong>NOTE</strong> Nordic semiconductor has a user guide that adafruit saved a copy of, it’s worth a read: <a href="https://cdn-learn.adafruit.com/assets/assets/000/059/041/original/nRF_Sniffer_User_Guide_v2.1.pdf?1533935335">https://cdn-learn.adafruit.com/assets/assets/000/059/041/original/nRF_Sniffer_User_Guide_v2.1.pdf?1533935335</a></p>
</blockquote>

<h2 id="sniffing-packets">Sniffing Packets</h2>

<p>During the initial sniffing to see what’s up, I was really frustrated by something so I want to highlight it right away. The sniffer is not guarenteed to catch every packet on the air. So sometimes, it will miss the connection request from the phone to the lightbulb. This means it can’t track that connection, and wireshark wont display any packets after the phone is connected, even if it’s sending lots of data back and forth. If you connect to the lightbulb (or other bluetooth LE device) and wireshark stops tracking it, close the app on your phone, wait for the lightbulb to start advertising again, and try to connect again.</p>

<h3 id="following-one-device">Following One Device</h3>
<p>Once the wireshark session is started, you’ll probably be slammed with a bunch of advertising packets from every peripheral device that isn’t currently connected. The nordic add-on for wireshark has a drop down labeled “Device”, which lists the Mac address of all advertising addresses. Select the lightbulb <code class="language-plaintext highlighter-rouge">50:33:8B:10:9B:EB</code> and it’ll only display those packets.</p>

<p><img src="/assets/bluetooth-sniffing/wireshark_advertising_packets.png" alt="Screenshot of wireshark displaying captured packets" /></p>

<p>So now it only displays the lightbulb’s packets, but there’s still a lot to read through. Let’s start using some filters.</p>

<h3 id="parsing-a-capture">Parsing a capture</h3>

<p>Like I said earlier, I don’t care about the advertising and connecting process. I can’t imagine a lightbulb would have a non-standard connection, so I’m just going to ignore it for now. Wireshark lets you filter out packets by writing statements in the “Apply a display filter” textbox at the top. To use a filter to remove advertising packets, type in <code class="language-plaintext highlighter-rouge">!btle.advertising_address</code> and hit enter.</p>

<p>This makes it a bit more readable, but still has packets with protocol “LE LL”, which stands for bluetooth Low Energy Link Layer. This is responsible for advertising, scanning, and creating/maintaining connections, and other protocols rely on it. So again, I don’t think I care about this layer. In fact, I really care about the GATT stuff, so lets only display that. The display filter for that is just <code class="language-plaintext highlighter-rouge">btatt</code>.</p>

<p><img src="/assets/bluetooth-sniffing/wireshark_btattFilter.png" alt="Screenshot of wireshark displaying packets after they've been filtered by the btatt filter. There are multiple &quot;Find information request&quot; lines in the info column, as well as &quot;Sent Write Command&quot;" /></p>

<blockquote>
  <p><strong>NOTE</strong> The Source column switches to “Master”/”Slave” terminology automatically. The “Master” is phone, “the slave” is the lightbulb. I’m not sure how to change that text to a more descriptive option like “Central” and “peripheral” or “client” and “server”, unfortunately.</p>
</blockquote>

<p>This is a more manageable list. Poking around, we can see initially the phone is sending info requests about different services and characteristics. Then, right at the end of the capture, there’s a bunch of “sent write command” to the <code class="language-plaintext highlighter-rouge">0x002e</code> handle. Changing the filter to <code class="language-plaintext highlighter-rouge">btatt.opcode == 0x52</code> filters the <code class="language-plaintext highlighter-rouge">ATT</code> packets down to just the ones sending write commands. The info column gives me the handle it’s writing to, but not the value it’s sending. I can add the value as a column by opening up the packet in the bottom window (aka click on a write packet), right-click on the value, and select “apply as column”.</p>

<p><img src="/assets/bluetooth-sniffing/wireshark_writePackets_withValueColumn.png" alt="Screenshot of wireshark with filter, and new &quot;value&quot; column. The first two packets have random looking values, and after that all values follow a simple looking pattern" /></p>

<blockquote>
  <p><strong>NOTE</strong> I did some cheating on the screenshots, and they’re actually from the capture when I did my testing below. So if you notice any patterns already, pretend you didn’t yet, I’ll talk more about that in the next section!</p>
</blockquote>

<p>Now that I’ve poked around some, let’s do some testing.</p>

<h3 id="hypothesis-and-testing">Hypothesis and Testing</h3>
<p>The first thing I want to figure out is something I would think is the simplest: setting the color of the lightbulb. The lightbulb app lets you set the color by selecting a point on a color wheel and set brightness with a slider bar at the bottom of the screen. The RGB of the color you select is displayed in the upper left. You can also save up to 5 specific color/brightness settings and switch to them with a touch of a button. Here’s a screenshot:</p>

<p><img src="/assets/bluetooth-sniffing/Screenshot_magicLight_ColorPicker.png" alt="Screenshot of app as described above" /></p>

<p>So, <strong>my hypothesis</strong>: It’s probably sending color data via either RGB or Hex. To easiser find those packets, I can save specific colors, varying the color and brightness, and then sniff the connection while repeatedly cycling through my saved colors.</p>

<p>Here’s the color options I saved, their RGB, Hex, and Brightness values:</p>

<table>
  <thead>
    <tr>
      <th> </th>
      <th>RGB</th>
      <th>Hex</th>
      <th>Brightness</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>option one</td>
      <td>100, 255, 50</td>
      <td>64ff32</td>
      <td>100%</td>
    </tr>
    <tr>
      <td>option two</td>
      <td>255, 101, 53</td>
      <td>ff6535</td>
      <td>100%</td>
    </tr>
    <tr>
      <td>option three</td>
      <td>102, 51, 255</td>
      <td>6633ff</td>
      <td>100%</td>
    </tr>
    <tr>
      <td>option four</td>
      <td>51, 25, 128</td>
      <td>331980</td>
      <td>50%</td>
    </tr>
  </tbody>
</table>

<blockquote>
  <p><strong>NOTE</strong>: I made option 3, then set the brightness to 50% and then set that as option four. So it should have had the same RGB values. Since it didn’t, that must mean the app code changed the RGB value based on brightness. So brightness may just be a function of RGB and not a separate variable at all.</p>
</blockquote>

<p>With that set, I disconnected from the lightbulb, started a wireshark session, connected, and cycled through the saved colors I just added, going through them in order 3 times.</p>

<p>I stopped the capture and did the filtering I talked about above. Looking at the write commands to the <code class="language-plaintext highlighter-rouge">0x02e</code> handle, and just the values sent, I see a pattern:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Value 
5664ff3200f0aa
56ff653500f0aa
566633ff00f0aa
5633198000f0aa

5664ff3200f0aa
56ff653500f0aa
566633ff00f0aa
5633198000f0aa

5664ff3200f0aa
56ff653500f0aa
566633ff00f0aa
5633198000f0aa
</code></pre></div></div>

<p>All values start with 56 and end with 00f0aa. I cycled through the 4 options three times in a row, and I can see these values repeating three times. Well <em>that</em> seems awfully suspicious…</p>

<h3 id="sending-custom-commands">Sending Custom Commands</h3>
<p>As it turns out, my laptop already has all the hardware and software needed to send custom bluetooth LE packets! It’s a linux box, and already has bluez on it, as well as gattool, hciconfig, and hcitool. If you also have a linux box but not the tools yet, Jared Wolf has a blog post about how to install and set them up: <a href="https://www.jaredwolff.com/get-started-with-bluetooth-low-energy/">https://www.jaredwolff.com/get-started-with-bluetooth-low-energy/</a>.</p>

<p>If you don’t have a linux box, Adafruit does have a tutorial on how to install the bluez stack on a raspberry pi here: <a href="https://learn.adafruit.com/pibeacon-ibeacon-with-a-raspberry-pi/compiling-bluez">https://learn.adafruit.com/pibeacon-ibeacon-with-a-raspberry-pi/compiling-bluez</a>.</p>

<p>Basic steps for use:</p>
<ol>
  <li>Make sure bluetooth is turned on on computer! In my case, I go to where I set the wifi and make sure the little bluetooth symbol has the word “On” next to it.</li>
  <li>In terminal, run <code class="language-plaintext highlighter-rouge">hcitool dev</code>, which shows any bluetooth hardware connected to my laptop, and it’s status. I took a screenshot of running this command when the bluetooth is turned off, then turned on, to see the difference:
<img src="/assets/bluetooth-sniffing/terminal_hcitoolDev_bluetoothOffAndOn.png" alt="Screenshot of terminal" /></li>
  <li>Running <code class="language-plaintext highlighter-rouge">sudo hcitool lescan</code> does a low energy scan with the default bluetooth device (my laptop’s <code class="language-plaintext highlighter-rouge">hci0</code> device) and shows the IDs of any broadcasting peripherals. I spotted the <code class="language-plaintext highlighter-rouge">50:33:8B:10:9B:EB</code> device so I know my laptop is within range.
    <blockquote>
      <p><strong>NOTE</strong> if <code class="language-plaintext highlighter-rouge">sudo hcitool lescan</code> doesn’t return anything, you may have to turn off bluetooth and turn it back on again. I had that happen a couple times and it was unclear why that happened.</p>
    </blockquote>
  </li>
  <li>Run <code class="language-plaintext highlighter-rouge">sudo gatttool -I</code> to start an interactive gatttool instance, and then enter <code class="language-plaintext highlighter-rouge">connect 50:33:8B:10:9B:EB</code> to connect and start interactive session with lightbulb. For help with gatttool, type <code class="language-plaintext highlighter-rouge">help</code></li>
</ol>

<p>Using the gatt tool’s <code class="language-plaintext highlighter-rouge">characteristics</code> command I get a list of all characteristics and their UUIDs. The nordic app listed the RGBW 4bytes characteristic as a UUID of 0xFFE9. The <code class="language-plaintext highlighter-rouge">characteristics</code> command gives this line with that UUID:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>handle: 0x002d, char properties: 0x04, char value handle: 0x002e, uuid: 0000ffe9-0000-1000-8000-00805f9b34fb
</code></pre></div></div>

<p>This means the characteristic value’s handle is 0x002e, and it’s the one I’d want to write to if I want to update the RGBW 4bytes characteristic. It also matches up nicely with wireshark’s packet capture. So the lightbulb is sending data to the RGBW 4byte characteristic! This further supports my “it’s sending hex to update the color” hypothesis.</p>

<p>If I only had the wireshark data, I could also work backwards to find out the characteristic it’s writing to. Running <code class="language-plaintext highlighter-rouge">primary</code> gives me a list of primary services, and the range of handles for each. Looking at that I can see the <code class="language-plaintext highlighter-rouge">0xffe5</code> service has handles <code class="language-plaintext highlighter-rouge">0x0023</code> to <code class="language-plaintext highlighter-rouge">0x0033</code>, so the <code class="language-plaintext highlighter-rouge">0x002e</code> handle is in that service. Relevant line:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>attr handle: 0x0023, end grp handle: 0x0033 uuid: 0000ffe5-0000-1000-8000-00805f9b34fb
</code></pre></div></div>

<p>So I can then run <code class="language-plaintext highlighter-rouge">characteristics 0x0023 0x0033</code> and see all characteristics:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>handle: 0x0024, char properties: 0x0a, char value handle: 0x0025, uuid: 0000ffe6-0000-1000-8000-00805f9b34fb
handle: 0x0027, char properties: 0x0a, char value handle: 0x0028, uuid: 0000ffe7-0000-1000-8000-00805f9b34fb
handle: 0x002a, char properties: 0x0a, char value handle: 0x002b, uuid: 0000ffe8-0000-1000-8000-00805f9b34fb
handle: 0x002d, char properties: 0x04, char value handle: 0x002e, uuid: 0000ffe9-0000-1000-8000-00805f9b34fb
handle: 0x0030, char properties: 0x0a, char value handle: 0x0031, uuid: 0000ffea-0000-1000-8000-00805f9b34fb
</code></pre></div></div>
<p>And I can see the characteristic value handle of 0x002e is for characteristic with handle 0x002d! I can also see that the next characteristic’s handle is <code class="language-plaintext highlighter-rouge">0x0030</code>, not <code class="language-plaintext highlighter-rouge">0x002f</code>. Handles don’t have to be consecutive, but this may mean there’s a characteristic descriptor after the characteristic value. I can search for this by doing <code class="language-plaintext highlighter-rouge">char-desc 0x002e 0x0030</code>. And it gets me a result, there is a handle 0x002f, and the UUID of <code class="language-plaintext highlighter-rouge">00002901-0000-1000-8000-00805f9b34fb</code> means it’s a “Characteristic User Description”. Let’s read it with <code class="language-plaintext highlighter-rouge">char-read-hnd 0x002f</code>, which gives us:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>Characteristic value/descriptor: 52 47 42 57 20 34 20 62 79 74 65 73 
</code></pre></div></div>
<p>And that hex value, when dropped into a hex to ASCII converter, gives us “RGBW 4 bytes”. Yeah that’s a lot of work, I think I’ll stick with the phone app for exploration. Moving on to the fun bit!</p>

<p>I know I should send a write command to the <code class="language-plaintext highlighter-rouge">0x002e</code> handle, and if I want to send a color, I need to have the hex value, surrounded by unknown, seemingly hard-coded values. So sending two different colors would be:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>char-write-cmd 0x002e 5664ff3200f0aa
char-write-cmd 0x002e 5633198000f0aa
</code></pre></div></div>

<p>Hit enter on one of them, and…IT WORKS!!!!!!</p>

<p>After I danced around a bit, I tested it a bit more. Looks like the command to send arbitrary color, where XXXXXX is hex color, is: <code class="language-plaintext highlighter-rouge">char-write-cmd 0x002e 56XXXXXX00f0aa</code>. The extra bytes before and after the hex color don’t seem to matter much, though without them it doesn’t work. Something to signify to the lightbulb I’m sending a hex value, I guess.</p>

<h3 id="other-commands">Other Commands</h3>
<p>Using the same process described above, I also looked into the ability to turn the lightbulb on and off, and the default “Warm White” and “Cool White” settings. They all <em>also</em> send to the <code class="language-plaintext highlighter-rouge">0x002e</code> handle, just sending different values. My discoveries:</p>

<ul>
  <li>Turn off Light: <code class="language-plaintext highlighter-rouge">char-write-cmd 0x002e cc2433</code></li>
  <li>Turn on Light: <code class="language-plaintext highlighter-rouge">char-write-cmd 0x002e cc2333</code></li>
  <li>Set to “Warm white”: <code class="language-plaintext highlighter-rouge">char-write-cmd 0x002e 56000000ff0faa</code></li>
  <li>Set to “Cool White”: <code class="language-plaintext highlighter-rouge">char-write-cmd 0x002e 56ffffff00f0aa</code></li>
</ul>

<p>For the warm white, I can see it sets the hex color to all zeros, but sets the two zeros after the hex color to <code class="language-plaintext highlighter-rouge">ff</code>. This implies to me there’s actually two sets of lights in there. One does the color, and one does the “Warm white” light. Sadly I only have one lightbulb, otherwise I’d be tempted to do a teardown to see if I’m right…</p>

<h2 id="conclusion">Conclusion</h2>

<p>I had my first foray into bluetooth Low Energy, and got to do some real reverse engineering! I haven’t figured out the timers yet, since it’s sending more complicated data, and I need to do more mucking about to decode what it’s sending. But this is a good place to end this post (It’s getting long!), so if I do figure it out, it’ll be a seperate post. Doing this was equal parts fun and frustrating, and I definitely feel like I understand bluetooth LE way more than I did before.</p>

<h2 id="resources">Resources</h2>

<p>Adafruit has an intro to bluetooth page if you want a good overview: <a href="https://learn.adafruit.com/introduction-to-bluetooth-low-energy">https://learn.adafruit.com/introduction-to-bluetooth-low-energy</a></p>

<p>Bluetooth’s developer guide: <a href="https://www.bluetooth.com/blog/a-developers-guide-to-bluetooth/">https://www.bluetooth.com/blog/a-developers-guide-to-bluetooth/</a></p>

<p>General bluetooth low energy info:<a href="https://www.bluetooth.com/bluetooth-resources/bluetooth-le-developer-starter-kit/">https://www.bluetooth.com/bluetooth-resources/bluetooth-le-developer-starter-kit/</a></p>

<p>Bluetooth Low Energy in 10 minutes video: <a href="https://www.youtube.com/watch?v=JSQhRyTKnW4">https://www.youtube.com/watch?v=JSQhRyTKnW4</a></p>

<p>Nordic semiconductors “devzone” site has some well written info about low energy too: <a href="https://devzone.nordicsemi.com/nordic/short-range-guides/b/bluetooth-low-energy/posts/ble-characteristics-a-beginners-tutorial">https://devzone.nordicsemi.com/nordic/short-range-guides/b/bluetooth-low-energy/posts/ble-characteristics-a-beginners-tutorial</a></p>

<p>Getting Started with Bluetooth Low Energy from Oreilly has chapter 4 online, which is about GATT: <a href="https://www.oreilly.com/library/view/getting-started-with/9781491900550/ch04.html">https://www.oreilly.com/library/view/getting-started-with/9781491900550/ch04.html</a></p>

<p>Good writeup on using <code class="language-plaintext highlighter-rouge">gatttool</code> and <code class="language-plaintext highlighter-rouge">hcitool</code> to connect to bluetooth LE devices: <a href="https://www.jaredwolff.com/get-started-with-bluetooth-low-energy/#connect-to-your-bluetooth-low-energy-device">https://www.jaredwolff.com/get-started-with-bluetooth-low-energy/#connect-to-your-bluetooth-low-energy-device</a></p>

<p>Bluetooth has a list of 16-bit UUID Numbers and what they map to. It used to be all on searchable webpages, but they’ve moved it to a PDF: <a href="https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf">https://btprodspecificationrefs.blob.core.windows.net/assigned-values/16-bit%20UUID%20Numbers%20Document.pdf</a></p>]]></content><author><name></name></author><category term="technical write up" /><category term="side project" /><category term="How-to" /><category term="reverse engineering" /><category term="side project" /><category term="embedded" /><summary type="html"><![CDATA[“Smart” things are the wave of the future! Everyone wants some bit of tech that already exists, like a lightbulb, but now controlled through an app! Or at least, that’s what marketers have decided on. But sometimes the companies maintaining the app goes under, or decides to not support what you bought anymore, and your fancy smart lightbulb becomes, well, a $35 lightbulb. I recently got my hands on an old bluetooth lightbulb that was first made in 2015. But the phone app to control it is clunky and ugly, and hasn’t been updated since 2018. So if I want to still use it now, the options are to have an unmaintaned, potential security-risk app on my phone, or not use it. Or, be me, since I’ve been itching to learn more about bluetooth low energy. In which case, it becomes time to do some reverse engineering!]]></summary></entry><entry><title type="html">Using Interrupts on the Adafruit Feather Huzzah with ESP8266</title><link href="/blog/2021/9/8/interrupts-with-adafruit-feather-huzzah-esp8266" rel="alternate" type="text/html" title="Using Interrupts on the Adafruit Feather Huzzah with ESP8266" /><published>2021-09-08T00:00:00+00:00</published><updated>2021-09-08T00:00:00+00:00</updated><id>/blog/2021/9/8/interrupts-with-adafruit-feather-huzzah-esp8266</id><content type="html" xml:base="/blog/2021/9/8/interrupts-with-adafruit-feather-huzzah-esp8266"><![CDATA[<p>It doesn’t take long learning embedded systems before you come across interrupts. Learning how to make your embedded system quickly react to changes in the real world (button presses, motion sensing, whatever) is often a default requirement for any fun projects. Since I’ve been programming on the Adafruit Feather Huzzah with ESP8266 (a mouthful, I have to say), I decided to use that for some hands-on learning.</p>

<p><img src="/assets/interrupts-with-esp8266-feather/feather_interrupt.jpg" alt="Adafruit Feather Huzzah on a breadboard with a switch, LED, a resistor, and some wires" />
<!--more--></p>

<h2 id="the-plan">The Plan</h2>
<p>Do some basic interrupts and learn more about how to use them with the ESP8266. A basic interrupt example that I’ve seen around a lot is turning on an LED for a certain amount of time when a button is pressed. So let’s do that!</p>

<blockquote>
  <p><strong>NOTE</strong> I’m assuming in this post you already have the setup to run the Arduino IDE and write programs for the ESP8266.</p>
</blockquote>

<h2 id="hardware">Hardware</h2>

<p>Hardware needed:</p>
<ul>
  <li><a href="https://www.adafruit.com/product/3213">Adafruit Feather Huzzah w/ ESP8266, with soldered on headers</a></li>
  <li>Breadboard</li>
  <li>Jumper wires</li>
  <li>LED</li>
  <li>300 Ohm Resistor</li>
</ul>

<p>To wire up the circuit I’ll be using, attach the LED and resistor in series, and connect to pin 14 and ground on the feather. Then attach the momentary switch to pin 4 and ground. Below is a simple drawing showing the circuit. The wire color in the diagram is just for visual clarity.</p>

<p><img src="/assets/interrupts-with-esp8266-feather/schematic.png" alt="Schematic drawing" /></p>

<h2 id="without-interrupts">Without Interrupts</h2>
<p>First, lets get the basic functionality working without interrupts. Note that I set the momentary switch as a pull up. This means when the switch is not pressed, I’ll read the pin as <code class="language-plaintext highlighter-rouge">HIGH</code>, and when it’s pressed, it’ll go to <code class="language-plaintext highlighter-rouge">LOW</code>. If I didn’t set it, the pin would not be guaranteed either <code class="language-plaintext highlighter-rouge">HIGH</code> or <code class="language-plaintext highlighter-rouge">LOW</code>, and reading the state of pin 4 wouldn’t tell me if the button was pushed or not!</p>

<p>I also used the <code class="language-plaintext highlighter-rouge">millis()</code> function instead of <code class="language-plaintext highlighter-rouge">delay()</code>. This is because <code class="language-plaintext highlighter-rouge">delay</code> doesn’t let me do anything else while I’m waiting for the LED to finish being on. If I want to use this code to do other things while the LED is on, I have to instead track the time since it was turned on, so I can do other things in the <code class="language-plaintext highlighter-rouge">loop()</code> too.</p>

<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="cp">#define GPIO_INTERRUPT_PIN 4
#define LED_PIN 14
#define WAIT_TIME 500
</span>
<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">lastTrigger</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
<span class="kt">bool</span> <span class="n">buttonPressed</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>

<span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">//Start serial</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">115200</span><span class="p">);</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"Starting sketch"</span><span class="p">);</span>

  <span class="c1">//Set the LED pin to output</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">LED_PIN</span><span class="p">,</span> <span class="n">OUTPUT</span><span class="p">);</span>
  <span class="c1">//set the eventual interrupt pin as an input that is pulled up</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">GPIO_INTERRUPT_PIN</span><span class="p">,</span> <span class="n">INPUT_PULLUP</span><span class="p">);</span>

<span class="p">}</span>

<span class="kt">void</span> <span class="n">loop</span><span class="p">()</span> <span class="p">{</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">buttonPressed</span> <span class="o">&amp;&amp;</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">GPIO_INTERRUPT_PIN</span><span class="p">)</span> <span class="o">==</span> <span class="n">LOW</span><span class="p">){</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"Button pressed"</span><span class="p">);</span>
    <span class="n">digitalWrite</span><span class="p">(</span><span class="n">LED_PIN</span><span class="p">,</span> <span class="n">HIGH</span><span class="p">);</span>
    <span class="n">buttonPressed</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
    <span class="n">lastTrigger</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
  <span class="p">}</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">buttonPressed</span> <span class="o">&amp;&amp;</span> <span class="n">lastTrigger</span> <span class="o">+</span> <span class="n">WAIT_TIME</span> <span class="o">&lt;</span> <span class="n">millis</span><span class="p">()){</span>
    <span class="n">digitalWrite</span><span class="p">(</span><span class="n">LED_PIN</span><span class="p">,</span> <span class="n">LOW</span><span class="p">);</span>
    <span class="n">buttonPressed</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="c1">//Other functionality goes here...</span>

<span class="p">}</span></code></pre></figure>

<p>Of course most code isn’t only checking a button. Let’s add a <code class="language-plaintext highlighter-rouge">delay</code> in there to simulate doing other work.</p>

<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="cp">#define GPIO_INTERRUPT_PIN 4
#define LED_PIN 14
#define WAIT_TIME 500
</span>
<span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">lastTrigger</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
<span class="kt">bool</span> <span class="n">buttonPressed</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>

<span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">//Start serial</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">115200</span><span class="p">);</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"Starting sketch"</span><span class="p">);</span>

  <span class="c1">//Set the LED pin to output</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">LED_PIN</span><span class="p">,</span> <span class="n">OUTPUT</span><span class="p">);</span>
  <span class="c1">//set the eventual interrupt pin as an input that is pulled up</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">GPIO_INTERRUPT_PIN</span><span class="p">,</span> <span class="n">INPUT_PULLUP</span><span class="p">);</span>

<span class="p">}</span>

<span class="kt">void</span> <span class="n">loop</span><span class="p">()</span> <span class="p">{</span>

  <span class="k">if</span> <span class="p">(</span><span class="o">!</span><span class="n">buttonPressed</span> <span class="o">&amp;&amp;</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">GPIO_INTERRUPT_PIN</span><span class="p">)</span> <span class="o">==</span> <span class="n">LOW</span><span class="p">){</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"Button pressed"</span><span class="p">);</span>
    <span class="n">digitalWrite</span><span class="p">(</span><span class="n">LED_PIN</span><span class="p">,</span> <span class="n">HIGH</span><span class="p">);</span>
    <span class="n">buttonPressed</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
    <span class="n">lastTrigger</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
  <span class="p">}</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">buttonPressed</span> <span class="o">&amp;&amp;</span> <span class="n">lastTrigger</span> <span class="o">+</span> <span class="n">WAIT_TIME</span> <span class="o">&lt;</span> <span class="n">millis</span><span class="p">()){</span>
    <span class="n">digitalWrite</span><span class="p">(</span><span class="n">LED_PIN</span><span class="p">,</span> <span class="n">LOW</span><span class="p">);</span>
    <span class="n">buttonPressed</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
  <span class="p">}</span>

  <span class="c1">//Other functionality goes here...</span>
  <span class="n">delay</span><span class="p">(</span><span class="mi">500</span><span class="p">);</span>

<span class="p">}</span></code></pre></figure>

<p>If you upload this code to the feather, you can see the button press is erratic. First, if you mess with the <code class="language-plaintext highlighter-rouge">delay</code> time, you can see that once the number of milliseconds you delay is longer than <code class="language-plaintext highlighter-rouge">WAIT_TIME</code>, the LED will stay on longer. This is because turning off the LED is in the <code class="language-plaintext highlighter-rouge">loop()</code> function with everything else, so while it’s in the <code class="language-plaintext highlighter-rouge">delay</code>, the code can’t turn the LED off!</p>

<p>Second, it doesn’t always register that the button was pressed. When it’s in the <code class="language-plaintext highlighter-rouge">delay</code> function (aka doing other stuff), it’s not checking if the button has switched to <code class="language-plaintext highlighter-rouge">LOW</code> (aka been pressed). Completely missing a button press is not acceptable for pretty much <em>any</em> project! Let’s fix that.</p>

<h2 id="basics-of-interrupts">Basics of Interrupts</h2>

<p>Interrupts are a way to <em>interrupt</em> the standard flow of your program. In my particular case, I want to interrupt whatever I’m doing in my <code class="language-plaintext highlighter-rouge">loop()</code> function and turn on the LED when I press the button.</p>

<p>The steps of a generic interrupt happening are:</p>
<ol>
  <li>The ESP8266 is running code (both overhead and code in my <code class="language-plaintext highlighter-rouge">loop</code> function)</li>
  <li>Something happens that I told the ESP8266 in my <code class="language-plaintext highlighter-rouge">setup</code> function needs to trigger an interrupt</li>
  <li>The ESP8266 stops whatever is currently happening and saves the context so it can get back to it later</li>
  <li>The ESP8266 looks up what it’s supposed to do when the trigger happens (aka looks for my interrupt handler code)</li>
  <li>It runs my handler code</li>
  <li>Once that’s done, it pulls the saved context back into its proper place, and starts up whatever it was doing before the interrupt happened.</li>
</ol>

<p>I also made a timing diagram of the button press interrupt happening, to give a more visual represenation.</p>

<p><img src="/assets/interrupts-with-esp8266-feather/timing_diagram.png" alt="Timing diagram of what was just described" /></p>

<p>So, to make this interrupt I want a reality, I have some basic setup to do:</p>

<ol>
  <li>Define what I want to happen when the interrupt triggers (i.e. turn on the LED and set some variables to control how long the LED is on). This will be my interrupt handler function, also known as an interrupt service routine or ISR.</li>
  <li>Set how I want to trigger the interrupt. In my case, I want to trigger on a GPIO pin.</li>
  <li>Since I’m triggering off of a GPIO pin, I need to set what state I want to trigger on. Some example options would be rising (when it goes from <code class="language-plaintext highlighter-rouge">LOW</code> to <code class="language-plaintext highlighter-rouge">HIGH</code>), falling (when it goes from <code class="language-plaintext highlighter-rouge">HIGH</code> to <code class="language-plaintext highlighter-rouge">LOW</code>), or whenever there’s a change.</li>
</ol>

<h2 id="arduino-specifics">Arduino Specifics</h2>
<p>Arduino has a builtin function <code class="language-plaintext highlighter-rouge">attachInterrupt</code> that lets me set what pin and when to trigger the interrupt. They have a write up here: <a href="https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/">https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/</a>. It’s pretty straightforward. Call <code class="language-plaintext highlighter-rouge">attachInterrupt</code> with the pin to watch, the function to call when it triggers, and when to trigger it.</p>

<p>Arduino has default constants you can use for when to trigger the interrupts. They are:</p>

<blockquote>
  <p>LOW to trigger the interrupt whenever the pin is low,</p>

  <p>CHANGE to trigger the interrupt whenever the pin changes value</p>

  <p>RISING to trigger when the pin goes from low to high,</p>

  <p>FALLING for when the pin goes from high to low.</p>
</blockquote>

<p>Since I want to trigger off my button attached to pin 4, and I want to trigger it when the pin goes from <code class="language-plaintext highlighter-rouge">LOW</code> to <code class="language-plaintext highlighter-rouge">HIGH</code> (aka when a person lifts their finger off the button), I can setup the interrupt with the below code:</p>

<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="n">attachInterrupt</span><span class="p">(</span><span class="n">digitalPinToInterrupt</span><span class="p">(</span><span class="n">GPIO_INTERRUPT_PIN</span><span class="p">),</span> <span class="n">detectsButton</span><span class="p">,</span> <span class="n">RISING</span><span class="p">);</span></code></pre></figure>

<p>That <code class="language-plaintext highlighter-rouge">detectsButton</code> argument is the name of the interrupt handler function. Which has to be defined a little different from what the arduino documentation says because I’m using the ESP8266.</p>

<h2 id="esp8266-specifics">ESP8266 Specifics</h2>

<p>The ESP8266 has a slightly different setup than the standard chips used in Arduinos, which results in an extra step. When declaring the interrupt function, you have to prepend <code class="language-plaintext highlighter-rouge">ICACHE_RAM_ATTR</code> to the function definition. This tells the linker to put this function in RAM instead of flash, where the rest of the program goes. The exact reasons why seem a bit murky. Different posts and forum questions online give no reason beyond “you gotta put this here”, or give different reasons why. But the basics as I understand it is that the interrupt handler (aka the code that runs during the interrupt) needs to be stored in RAM on the ESP8266, not flash, where it would normally be stored without that <code class="language-plaintext highlighter-rouge">ICACHE_RAM_ATTR</code>. This is because the interrupt code can run at any time, and if the chip is in the middle of writing or reading to flash, trying to also run the interrupt handler code in flash at the same time will cause the chip to crash. Crashing is bad, so the ESP8266 Arduino code has a built-in check to make sure all interrupt handlers are in the RAM. If it’s not, the code will pre-emptively crash with an error that says “ISR not in IRAM!”.</p>

<p>So the <code class="language-plaintext highlighter-rouge">detectsButton</code> function is defined as follows:</p>

<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="n">ICACHE_RAM_ATTR</span> <span class="kt">void</span> <span class="nf">detectsButton</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">//NOTE because in ISR, millis() call will return the same value every time</span>
  
  <span class="c1">//only trigger if I didn't just trigger DEBOUNCE_TIME ago</span>
  <span class="c1">//lastTrigger &gt; millis() means millis has overflowed</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">lastTrigger</span> <span class="o">&gt;</span> <span class="n">millis</span><span class="p">()</span> <span class="o">||</span> <span class="n">lastTrigger</span><span class="o">+</span><span class="n">DEBOUNCE_TIME</span> <span class="o">&lt;</span> <span class="n">millis</span><span class="p">())</span>
  <span class="p">{</span>
    <span class="n">lastTrigger</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span> 
    <span class="n">buttonPressed</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span><span class="p">{</span>
    <span class="n">bounce</span><span class="o">++</span><span class="p">;</span>
  <span class="p">}</span>
 
<span class="p">}</span></code></pre></figure>

<p>There’s a couple things to note here. One, my comment that starts with “NOTE” that talks about the <code class="language-plaintext highlighter-rouge">millis</code> function. The <code class="language-plaintext highlighter-rouge">millis()</code> function returns the number of milliseconds it’s been since the chip turned on. It actually uses interrupts in order to update! That interrupt is setup by the Arduino code, which is why I don’t have to setup that interrupt in my code. However, Arduino disables interrupts when you’re running an interrupt handler. Presumably so you don’t get interrupts triggering while you’re handling an interrupt and just get lost in nested interrupts forever. But this means the interrupt to update the return value for <code class="language-plaintext highlighter-rouge">millis()</code> wont run, and so it’ll return the same value the entire time you’re in an interrupt handler.</p>

<p>Second, there’s a check <code class="language-plaintext highlighter-rouge">lastTrigger+DEBOUNCE_TIME &lt; millis()</code> that happens before it actually updates that the button was pressed. The ESP8266 says that the button has changed from <code class="language-plaintext highlighter-rouge">LOW</code> to <code class="language-plaintext highlighter-rouge">HIGH</code>, which is why it’s running this code, so why am I adding extra checks? It’s because of debouncing. The analog world can be messy (citation needed), so when the button is first pressed, or first released, the reading on pin 4 can bounce between <code class="language-plaintext highlighter-rouge">LOW</code> and <code class="language-plaintext highlighter-rouge">HIGH</code> a bit before it settles down on one or the other. During that time, the interrupt can get triggered several times. But I only want it to actually trigger once! So I add a check in there. If the last time the button was triggered was <em>really</em> recent (aka within <code class="language-plaintext highlighter-rouge">DEBOUNCE_TIME</code> milliseconds), then I assume this trigger was the button bouncing a bit, and ignore it. Because I was curious, though, I don’t completely ignore it. Instead I set the <code class="language-plaintext highlighter-rouge">bounce++</code> to add to my total bounce amount. In my <code class="language-plaintext highlighter-rouge">loop</code> code I check that variable and output to serial that I bounced, just so I could see how often it happens.</p>

<p>Lastly, there’s a check for <code class="language-plaintext highlighter-rouge">lastTrigger &gt; millis()</code> which at first glance doesn’t make sense. The<code class="language-plaintext highlighter-rouge">lastTrigger</code> variable is set to <code class="language-plaintext highlighter-rouge">millis()</code> from the last time it was triggered, and the number of milliseconds since the chip was turned on will only go up (we’re not time-traveling!). So how can this check ever return true? The answer is overflow! The number of milliseconds since turn on is stored in an unsigned long variable. Since it’s unsigned, the variable is always positive, and since it’s a long, its size is 32 bits. This means the max number of milliseconds it can store is 2<sup>32</sup> - 1, aka 4294967295 milliseconds. Convert that to days, and you get 49.7103009259 days, aka around 50 days (which the Arduino documentation on <code class="language-plaintext highlighter-rouge">millis()</code> says). Once it gets to that number, and you add one more millisecond, the number is 33 bits long! So the code just gets rid of the topmost number, and suddenly you’re getting 0 milliseconds from <code class="language-plaintext highlighter-rouge">millis()</code>. Now, I’m unlikely to run this test code for 50 days, so I don’t <em>really</em> need it, but I like to be complete when I can.</p>

<h2 id="final-code">Final Code</h2>
<p>Now that I have all that preamble out of the way, I can put together final example code using interrupts on the ESP8266.</p>

<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="cp">#define GPIO_INTERRUPT_PIN 4
#define LED_PIN 14
#define WAIT_TIME 250
#define DEBOUNCE_TIME 10
</span>

<span class="k">volatile</span> <span class="kt">bool</span> <span class="n">buttonPressed</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
<span class="k">volatile</span> <span class="kt">unsigned</span> <span class="kt">long</span> <span class="n">lastTrigger</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span>
<span class="k">volatile</span> <span class="kt">int</span> <span class="n">bounce</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">prevBounceCount</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

<span class="n">ICACHE_RAM_ATTR</span> <span class="kt">void</span> <span class="nf">detectsButton</span><span class="p">()</span> <span class="p">{</span>
  <span class="c1">//NOTE because in ISR, millis() call will return the same value every time</span>
  
  <span class="c1">//only trigger if I didn't just trigger DEBOUNCE_TIME ago</span>
  <span class="c1">//lastTrigger &gt; millis() means millis has overflowed</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">lastTrigger</span> <span class="o">&gt;</span> <span class="n">millis</span><span class="p">()</span> <span class="o">||</span> <span class="n">lastTrigger</span><span class="o">+</span><span class="n">DEBOUNCE_TIME</span> <span class="o">&lt;</span> <span class="n">millis</span><span class="p">())</span>
  <span class="p">{</span>
    <span class="n">lastTrigger</span> <span class="o">=</span> <span class="n">millis</span><span class="p">();</span> 
    <span class="n">buttonPressed</span> <span class="o">=</span> <span class="nb">true</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span><span class="p">{</span>
    <span class="n">bounce</span><span class="o">++</span><span class="p">;</span>
  <span class="p">}</span>
 
<span class="p">}</span>

<span class="kt">void</span> <span class="n">setup</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">115200</span><span class="p">);</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"Starting sketch"</span><span class="p">);</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">LED_PIN</span><span class="p">,</span> <span class="n">OUTPUT</span><span class="p">);</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">GPIO_INTERRUPT_PIN</span><span class="p">,</span> <span class="n">INPUT_PULLUP</span><span class="p">);</span>

  <span class="n">attachInterrupt</span><span class="p">(</span><span class="n">digitalPinToInterrupt</span><span class="p">(</span><span class="n">GPIO_INTERRUPT_PIN</span><span class="p">),</span> <span class="n">detectsButton</span><span class="p">,</span> <span class="n">RISING</span><span class="p">);</span>

<span class="p">}</span>

<span class="kt">void</span> <span class="n">loop</span><span class="p">()</span> <span class="p">{</span>

  <span class="c1">//lastTrigger &gt; millis() means millis has overflowed</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">buttonPressed</span> <span class="o">&amp;&amp;</span> <span class="p">(</span><span class="n">lastTrigger</span> <span class="o">&gt;</span> <span class="n">millis</span><span class="p">()</span> <span class="o">||</span> <span class="n">lastTrigger</span><span class="o">+</span><span class="n">WAIT_TIME</span> <span class="o">&lt;=</span> <span class="n">millis</span><span class="p">())){</span>
     <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"button was pressed, time to turn off LED"</span><span class="p">);</span>
     <span class="n">digitalWrite</span><span class="p">(</span><span class="n">LED_PIN</span><span class="p">,</span> <span class="n">LOW</span><span class="p">);</span>
     <span class="n">buttonPressed</span> <span class="o">=</span> <span class="nb">false</span><span class="p">;</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">buttonPressed</span><span class="p">){</span>
    <span class="n">digitalWrite</span><span class="p">(</span><span class="n">LED_PIN</span><span class="p">,</span> <span class="n">HIGH</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">bounce</span> <span class="o">!=</span> <span class="n">prevBounceCount</span><span class="p">){</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"Debounced"</span><span class="p">);</span>
    <span class="n">prevBounceCount</span> <span class="o">=</span> <span class="n">bounce</span><span class="p">;</span>
  <span class="p">}</span>
  
  <span class="c1">//Other functionality goes here...</span>
  <span class="n">delay</span><span class="p">(</span><span class="mi">250</span><span class="p">);</span>

<span class="p">}</span></code></pre></figure>

<p>Notice how several variables are declared to be <code class="language-plaintext highlighter-rouge">volatile</code>. This is important! Setting variables to <code class="language-plaintext highlighter-rouge">volatile</code> is me telling the compiler that the variables may change at any time outside the standard code flow. I set them to <code class="language-plaintext highlighter-rouge">volatile</code> in the code so the compiler knows not to optimize them out, or to used a cached value. In this case, I’m setting several variables to <code class="language-plaintext highlighter-rouge">volatile</code> because I set them in the interrupt handler. Which means every time I access them in the <code class="language-plaintext highlighter-rouge">loop()</code> function, it’s possible that the interrupt had happened just before it, and changed those variables! This is important info for the compiler to know, and makes it so the compiled version of my code is actually what I want it to be. Embedded FM has a short article about volatile if you want to learn more: <a href="https://embedded.fm/blog/2017/2/23/explaining-the-c-keyword-volatile">https://embedded.fm/blog/2017/2/23/explaining-the-c-keyword-volatile</a></p>

<p>Now when a button is pressed (and released), the LED turns on immediately! Take some time to repeatedly press the button to turn on the LED and marvel at your new-found knowledge.
Note, however, that this works <em>completely</em> as intended only as long as the other functionality in the loop() doesn’t take longer than <code class="language-plaintext highlighter-rouge">WAIT_TIME</code> to complete. If the other functionality takes longer, than the <code class="language-plaintext highlighter-rouge">if</code> statement checking how long it’s been since last trigger won’t run in time, and the LED will stay on longer than <code class="language-plaintext highlighter-rouge">WAIT_TIME</code>. This is because turning the LED off is in the <code class="language-plaintext highlighter-rouge">loop</code> function, and so still restricted by all the other work done there. If you want an exact timing for how long the LED is on, then you’d have to add an interrupt that triggers on time, and that is outside the scope of this post. The ESP8266 does have timer interrupts, but that requires an extra library and more testing/playing around, so I’ll leave that as a potential future post.</p>

<p>In the meantime, enjoy your new knowledge, and go forth and interrupt things!</p>

<h2 id="references">References</h2>

<ul>
  <li>Arduino’s way of adding interrupts: <a href="https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/">https://www.arduino.cc/reference/en/language/functions/external-interrupts/attachinterrupt/</a></li>
  <li>In-depth article about Arduino interrupts (focused on AVR but applicable to ESP8266): <a href="http://gammon.com.au/interrupts">http://gammon.com.au/interrupts</a></li>
  <li>ESP8266 and Arduino specific interrupts: <a href="https://randomnerdtutorials.com/interrupts-timers-esp8266-arduino-ide-nodemcu/">https://randomnerdtutorials.com/interrupts-timers-esp8266-arduino-ide-nodemcu/</a></li>
  <li>Arduino Documentation for <code class="language-plaintext highlighter-rouge">millis()</code>: <a href="https://www.arduino.cc/reference/en/language/functions/time/millis/">https://www.arduino.cc/reference/en/language/functions/time/millis/</a></li>
  <li>ESP8266 Arduino interrupt documentation: <a href="https://arduino-esp8266.readthedocs.io/en/latest/reference.html#interrupts">https://arduino-esp8266.readthedocs.io/en/latest/reference.html#interrupts</a></li>
  <li>The book Making Embedded Systems by Elicia White has a section talking about interrupts that I referenced while writing this post: <a href="https://www.oreilly.com/library/view/making-embedded-systems/9781449308889/">https://www.oreilly.com/library/view/making-embedded-systems/9781449308889/</a></li>
</ul>]]></content><author><name></name></author><category term="technical write up" /><category term="side project" /><category term="How-to" /><category term="side project" /><category term="microcontrollers" /><category term="embedded" /><summary type="html"><![CDATA[It doesn’t take long learning embedded systems before you come across interrupts. Learning how to make your embedded system quickly react to changes in the real world (button presses, motion sensing, whatever) is often a default requirement for any fun projects. Since I’ve been programming on the Adafruit Feather Huzzah with ESP8266 (a mouthful, I have to say), I decided to use that for some hands-on learning.]]></summary></entry><entry><title type="html">How to use Quadrature Rotary Encoders</title><link href="/blog/2021/8/30/how-to-use-quadrature-rotary-encoders" rel="alternate" type="text/html" title="How to use Quadrature Rotary Encoders" /><published>2021-08-30T00:00:00+00:00</published><updated>2021-08-30T00:00:00+00:00</updated><id>/blog/2021/8/30/how-to-use-quadrature-rotary-encoders</id><content type="html" xml:base="/blog/2021/8/30/how-to-use-quadrature-rotary-encoders"><![CDATA[<p>As part of a project I’m working on, I wanted to have a nice rotating switch that let me flip between different options. Googling around, I discovered that apparently meant I needed a rotary encoder. I bought one from adafruit, only to realize I had no idea how to use it. And oddly enough, Adafruit didn’t have a pre-existing codebase I could pull from. It was time to read some datasheets, do some experiments, and figure out how to use it.</p>

<p><img src="https://cdn-shop.adafruit.com/970x728/377-02.jpg" alt="Rotary encoder with a black rubber knob on it, in front of a grey background" /></p>

<!--more-->

<h2 id="the-hardware">The Hardware</h2>

<p>I bought the <a href="https://www.adafruit.com/product/377">rotary encoder + extras</a> from adafruit. I had originally thought to grab one from digikey or elsewhere (if only to get used to buying from other places), but the sheer amount of variables I had to know about frazzled me. How many detents are enough? What are pulses per revolution? Adafruit just gives you one option. It’s a rotary encoder, what more do you need to know! Adafruit provides a one and done, and sometimes, that’s what you want to do. Once I got it, I put the knob on it and gave it a twirl. A very satisfying clicky twisting motion, exactly what I wanted! Now to do some reading.</p>

<p>Adafruit provides the <a href="https://cdn-shop.adafruit.com/datasheets/pec11.pdf">datasheet</a>, so I can see what’s what.</p>

<p>The 3 connector prongs on one side are the A, B and common (aka ground) channels. If I connect up to them and monitor A and B as I turn the knob, I should see the pattern specified in the Quadrature table.</p>

<p><img src="/assets/using-rotary-encoder/encoder-pins-from-datasheet.png" alt="Schematic drawing of the side of the rotary encoder, showing 3 prongs labeled A, B, and C. Next to the drawing it says A stands for Channel A, B for Channel B, and C for common" /></p>

<p><img src="/assets/using-rotary-encoder/encoder-signal-from-datasheet.png" alt="Schematic drawing showing the signal from channel A and B when turned clockwise or counterclockwise" /></p>

<p>Since I started only kind of understanding this information (and frankly was just confused by that signal schematic), I decided to do some testing. Since I wanted to use the encoder with the adafruit feather huzzah with ESP8266, I wired up the simplest circuit I could, connecting channel A on the encoder to pin 4, channel B to pin 5, and the common to ground. Diagram below:</p>

<p><img src="/assets/using-rotary-encoder/encoder-testing-circuit-diagram.png" alt="circuit diagram showing wiring just described" /></p>

<h2 id="the-first-test">The First Test</h2>
<p>The simplest test is to see what happens to things on channel A and B when I turn the knob. If you look back at the Quadrature table, you can see when both signals are HIGH, it means it’s off. And the way I wired it requires the pins to be pullups. So for this rotary encoder, when nothing is happening, they’re both set to HIGH, and when I turn the knob, they go down to low, one right after the other.</p>

<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="cp">#define PIN_ENCODER_A      4
#define PIN_ENCODER_B      5
</span>
<span class="kt">void</span> <span class="nf">setup</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">115200</span><span class="p">);</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">PIN_ENCODER_A</span><span class="p">,</span> <span class="n">INPUT_PULLUP</span><span class="p">);</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">PIN_ENCODER_B</span><span class="p">,</span> <span class="n">INPUT_PULLUP</span><span class="p">);</span>

<span class="p">}</span>

<span class="kt">void</span> <span class="n">loop</span><span class="p">()</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">valA</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_ENCODER_A</span><span class="p">);</span>
  <span class="kt">int</span> <span class="n">valB</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_ENCODER_B</span><span class="p">);</span>

  <span class="c1">//since these are default HIGH (aka have a pullup resistor)</span>
  <span class="c1">//either being set to LOW means something is happening on the encoder</span>
  <span class="k">if</span> <span class="p">(</span><span class="n">valA</span> <span class="o">==</span> <span class="n">LOW</span> <span class="o">||</span> <span class="n">valB</span> <span class="o">==</span> <span class="n">LOW</span><span class="p">){</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">valA</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">" "</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">valB</span><span class="p">);</span>
  <span class="p">}</span>

<span class="p">}</span></code></pre></figure>

<p>With this code running on my feather, I can turn the know clockwise and counter clockwise, and see what I get.</p>

<p>Turned clockwise, for each “click” of the knob, I get this:</p>

<table>
  <thead>
    <tr>
      <th>pin A</th>
      <th>pin B</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>1</td>
    </tr>
    <tr>
      <td>0</td>
      <td>1</td>
    </tr>
    <tr>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <td>1</td>
      <td>0</td>
    </tr>
    <tr>
      <td>1</td>
      <td>1</td>
    </tr>
  </tbody>
</table>

<p>If I want, I can just put those two values next to each other and get a binary number. So it goes from 11, to 01, to 00, to 10, to 11. With the decimal equivalent being: 3, 1, 0, 2, 3</p>

<p>So if I track the previous state and the current state, I can tell it’s going clockwise if any of these are true:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>previous state-&gt;current state
3-&gt;1
1-&gt;0
0-&gt;2
2-&gt;3
</code></pre></div></div>
<p>From this I can see I’ll have to at least track current and previous state, because it matters what state it’s coming from. Just knowing current state is “0” doesn’t mean anything about direction.</p>

<p>Let’s see if that pattern is different when it’s turned counter-clockwise. The values end up being:</p>

<table>
  <thead>
    <tr>
      <th>pin A</th>
      <th>pin B</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>1</td>
      <td>1</td>
    </tr>
    <tr>
      <td>1</td>
      <td>0</td>
    </tr>
    <tr>
      <td>0</td>
      <td>0</td>
    </tr>
    <tr>
      <td>0</td>
      <td>1</td>
    </tr>
    <tr>
      <td>1</td>
      <td>1</td>
    </tr>
  </tbody>
</table>

<p>Which is a different pattern! That’s good, otherwise I’d have to come up with a completely different way of figuring this out. In decimal, it would be: 3, 2, 0, 1, 3</p>

<p>So it’s going counterclockwise if:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>3-&gt;2
2-&gt;0
0-&gt;1
1-&gt;3
</code></pre></div></div>
<h2 id="second-test---lookup-tables">Second Test - Lookup tables</h2>

<p>Since I’ll have to do this in code eventually anyway, let’s do 1 to mean clockwise and -1 to mean counter clockwise. I can then list out all possible combinations of a previous and current state, and determine which way I’m going, or if I’m missing anything.</p>

<table>
  <thead>
    <tr>
      <th>previous state</th>
      <th>current state</th>
      <th>direction</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>0</td>
      <td>0</td>
      <td>UNDEFINED</td>
    </tr>
    <tr>
      <td>0</td>
      <td>1</td>
      <td>-1</td>
    </tr>
    <tr>
      <td>0</td>
      <td>2</td>
      <td>1</td>
    </tr>
    <tr>
      <td>0</td>
      <td>3</td>
      <td>UNDEFINED</td>
    </tr>
    <tr>
      <td>1</td>
      <td>0</td>
      <td>1</td>
    </tr>
    <tr>
      <td>1</td>
      <td>1</td>
      <td>UNDEFINED</td>
    </tr>
    <tr>
      <td>1</td>
      <td>2</td>
      <td>UNDEFINED</td>
    </tr>
    <tr>
      <td>1</td>
      <td>3</td>
      <td>-1</td>
    </tr>
    <tr>
      <td>2</td>
      <td>0</td>
      <td>-1</td>
    </tr>
    <tr>
      <td>2</td>
      <td>1</td>
      <td>UNDEFINED</td>
    </tr>
    <tr>
      <td>2</td>
      <td>2</td>
      <td>UNDEFINED</td>
    </tr>
    <tr>
      <td>2</td>
      <td>3</td>
      <td>1</td>
    </tr>
    <tr>
      <td>3</td>
      <td>0</td>
      <td>UNDEFINED</td>
    </tr>
    <tr>
      <td>3</td>
      <td>1</td>
      <td>1</td>
    </tr>
    <tr>
      <td>3</td>
      <td>2</td>
      <td>-1</td>
    </tr>
    <tr>
      <td>3</td>
      <td>3</td>
      <td>UNDEFINED</td>
    </tr>
  </tbody>
</table>

<p>Based on this, I can see there’s a lot of options that don’t mean the encoder went clockwise OR counter clockwise. Looking at those undefined values, there’s two separate groups. If the previous and current values from the encoder are the same, that just means I read the values so fast, it didn’t have time to change (or in the case of 3, the encoder isn’t moving at all). So those just mean it didn’t move. For the other group of undefined values, they’re harder to categorize. Going from 0 to 3 means EITHER I moved clockwise so fast I went from 0 to 2 to 3 before my code had a chance to read the encoder values, OR I moved it <em>counter</em> clockwise so fast I went from 0 to <strong>1</strong> to 3. So that tells me the encoder rotated, but I don’t know which way! I’m not sure how to solve that, so I’m just going to treat that as a different “SKIPPED VALUE” category.</p>

<p>I can reorganize the data to make it a bit more readable (at least to me), by putting the previous value as the first column, and the current value as the first row, like this:</p>

<table>
  <tbody>
    <tr>
      <td> </td>
      <td>0</td>
      <td>1</td>
      <td>2</td>
      <td>3</td>
    </tr>
    <tr>
      <td>0</td>
      <td>DIDN’T MOVE</td>
      <td>-1</td>
      <td>1</td>
      <td>SKIPPED A VALUE</td>
    </tr>
    <tr>
      <td>1</td>
      <td>1</td>
      <td>DIDN’T MOVE</td>
      <td>SKIPPED A VALUE</td>
      <td>-1</td>
    </tr>
    <tr>
      <td>2</td>
      <td>-1</td>
      <td>SKIPPED A VALUE</td>
      <td>DIDN’T MOVE</td>
      <td>1</td>
    </tr>
    <tr>
      <td>3</td>
      <td>SKIPPED A VALUE</td>
      <td>1</td>
      <td>-1</td>
      <td>DIDN’T MOVE</td>
    </tr>
  </tbody>
</table>

<p>Well hey, that looks an awful lot like a lookup table, a 4x4 array with the indices corresponding to the values. Let’s do that! For the “DIDN’T MOVE” category, I can just set it to 0, and for when I skip values, I’ll do 2, a clearly wrong value. With that, I get the below table:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>{0, -1, 1, 2}
{1, 0, 2, -1}
{-1, 2, 0, 1}
{2, 1, -1, 0}
</code></pre></div></div>

<p>With this planning, I can now try some code out and see if it works.</p>

<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="cp">#define PIN_ENCODER_A      4
#define PIN_ENCODER_B      5
</span>
<span class="kt">int</span> <span class="n">prevVal</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">newVal</span><span class="p">;</span>

<span class="kt">int</span> <span class="n">lookupTable</span><span class="p">[</span><span class="mi">4</span><span class="p">][</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span> <span class="p">{</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">},</span>
                          <span class="p">{</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">},</span>
                          <span class="p">{</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">},</span>
                          <span class="p">{</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">}</span> <span class="p">};</span>

<span class="kt">void</span> <span class="n">setup</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">115200</span><span class="p">);</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">PIN_ENCODER_A</span><span class="p">,</span> <span class="n">INPUT_PULLUP</span><span class="p">);</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">PIN_ENCODER_B</span><span class="p">,</span> <span class="n">INPUT_PULLUP</span><span class="p">);</span>

  <span class="kt">int</span> <span class="n">valA</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_ENCODER_A</span><span class="p">);</span>
  <span class="kt">int</span> <span class="n">valB</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_ENCODER_B</span><span class="p">);</span>
  <span class="n">prevVal</span> <span class="o">=</span> <span class="p">(</span><span class="n">valA</span> <span class="o">&lt;&lt;</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="n">valB</span><span class="p">;</span>

<span class="p">}</span>

<span class="kt">void</span> <span class="n">loop</span><span class="p">()</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">valA</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_ENCODER_A</span><span class="p">);</span>
  <span class="kt">int</span> <span class="n">valB</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_ENCODER_B</span><span class="p">);</span>
  <span class="n">newVal</span> <span class="o">=</span> <span class="p">(</span><span class="n">valA</span> <span class="o">&lt;&lt;</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="n">valB</span><span class="p">;</span>

  <span class="kt">int</span> <span class="n">info</span> <span class="o">=</span> <span class="n">lookupTable</span><span class="p">[</span><span class="n">prevVal</span><span class="p">][</span><span class="n">newVal</span><span class="p">];</span>


  <span class="k">if</span> <span class="p">(</span><span class="n">info</span> <span class="o">==</span> <span class="mi">1</span><span class="p">){</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"clockwise "</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">prevVal</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"-&gt;"</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">newVal</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">info</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">){</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"counter clockwise "</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="n">prevVal</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">print</span><span class="p">(</span><span class="s">"-&gt;"</span><span class="p">);</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="n">newVal</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">info</span> <span class="o">==</span> <span class="mi">2</span><span class="p">){</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"skipped a value"</span><span class="p">);</span>
  <span class="p">}</span>
  
  <span class="n">prevVal</span> <span class="o">=</span> <span class="n">newVal</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>

<h2 id="final-code---tracking-state">Final Code - Tracking State</h2>

<p>This correctly understands clockwise and counterclockwise movement. HOWEVER, there’s two issues. One, it’s triggering roughly 4 times each detent, and Two sometimes the code manages to get a bounce or something else wonky, and it reads, say, 3 clockwise and one counterclockwise movement while I’m moving it clockwise. So I want to track all the states it passes through, and then figure out which way its going, ignoring those occasional misreads.</p>

<blockquote>
  <p><strong>NOTE</strong> “detent” is a fancy word for that “click” feeling you get with rotary encoders. Each “click” position is a detent.</p>
</blockquote>

<figure class="highlight"><pre><code class="language-c--" data-lang="c++"><span class="cp">#define PIN_ENCODER_A      4
#define PIN_ENCODER_B      5
</span>
<span class="kt">int</span> <span class="n">prevVal</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">int</span> <span class="n">newVal</span><span class="p">;</span>
<span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">clockState</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
<span class="kt">unsigned</span> <span class="kt">int</span> <span class="n">counterClockState</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>

<span class="c1">//lookup table, first index is previous value</span>
<span class="c1">//second index is current value</span>
<span class="c1">//says if it's part of the sequence when moving</span>
<span class="c1">//clockwise (1) or counterclockwise (-1)</span>
<span class="c1">//didn't move (0) or skipped a value (2)</span>
<span class="kt">int</span> <span class="n">lookupTable</span><span class="p">[</span><span class="mi">4</span><span class="p">][</span><span class="mi">4</span><span class="p">]</span> <span class="o">=</span> <span class="p">{</span> <span class="p">{</span><span class="mi">0</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">},</span>
                          <span class="p">{</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">},</span>
                          <span class="p">{</span><span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">2</span><span class="p">,</span> <span class="mi">0</span><span class="p">,</span> <span class="mi">1</span><span class="p">},</span>
                          <span class="p">{</span><span class="mi">2</span><span class="p">,</span> <span class="mi">1</span><span class="p">,</span> <span class="o">-</span><span class="mi">1</span><span class="p">,</span> <span class="mi">0</span><span class="p">}</span> <span class="p">};</span>

<span class="kt">void</span> <span class="n">setup</span><span class="p">()</span> <span class="p">{</span>
  <span class="n">Serial</span><span class="p">.</span><span class="n">begin</span><span class="p">(</span><span class="mi">115200</span><span class="p">);</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">PIN_ENCODER_A</span><span class="p">,</span> <span class="n">INPUT_PULLUP</span><span class="p">);</span>
  <span class="n">pinMode</span><span class="p">(</span><span class="n">PIN_ENCODER_B</span><span class="p">,</span> <span class="n">INPUT_PULLUP</span><span class="p">);</span>

  <span class="kt">int</span> <span class="n">valA</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_ENCODER_A</span><span class="p">);</span>
  <span class="kt">int</span> <span class="n">valB</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_ENCODER_B</span><span class="p">);</span>
  <span class="n">prevVal</span> <span class="o">=</span> <span class="p">(</span><span class="n">valA</span> <span class="o">&lt;&lt;</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="n">valB</span><span class="p">;</span>

<span class="p">}</span>

<span class="kt">void</span> <span class="n">loop</span><span class="p">()</span> <span class="p">{</span>
  <span class="kt">int</span> <span class="n">valA</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_ENCODER_A</span><span class="p">);</span>
  <span class="kt">int</span> <span class="n">valB</span> <span class="o">=</span> <span class="n">digitalRead</span><span class="p">(</span><span class="n">PIN_ENCODER_B</span><span class="p">);</span>
  <span class="n">newVal</span> <span class="o">=</span> <span class="p">(</span><span class="n">valA</span> <span class="o">&lt;&lt;</span> <span class="mi">1</span><span class="p">)</span> <span class="o">+</span> <span class="n">valB</span><span class="p">;</span>
  
  <span class="kt">int</span> <span class="n">info</span> <span class="o">=</span> <span class="n">lookupTable</span><span class="p">[</span><span class="n">prevVal</span><span class="p">][</span><span class="n">newVal</span><span class="p">];</span>


  <span class="k">if</span> <span class="p">(</span><span class="n">info</span> <span class="o">==</span> <span class="mi">1</span><span class="p">){</span>
    <span class="n">clockState</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">newVal</span><span class="p">);</span> <span class="c1">//set the bit to 1</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">info</span> <span class="o">==</span> <span class="o">-</span><span class="mi">1</span><span class="p">){</span>
    <span class="n">counterClockState</span> <span class="o">|=</span> <span class="p">(</span><span class="mi">1</span> <span class="o">&lt;&lt;</span> <span class="n">newVal</span><span class="p">);</span>
  <span class="p">}</span>
  <span class="k">else</span> <span class="k">if</span> <span class="p">(</span><span class="n">info</span> <span class="o">==</span> <span class="mi">2</span><span class="p">){</span>
    <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"skipped a value"</span><span class="p">);</span>
  <span class="p">}</span>

  <span class="k">if</span> <span class="p">(</span><span class="n">prevVal</span> <span class="o">!=</span> <span class="n">newVal</span> <span class="o">&amp;&amp;</span> <span class="n">newVal</span> <span class="o">==</span> <span class="mi">3</span><span class="p">){</span>
    <span class="c1">//changed to the non moving state, lets figure out what direction we went!</span>

    <span class="c1">//for each clockwise and counterclockwise, the encoder state goes through 4 distinct states</span>
    <span class="c1">//make sure it's gone through at least 3 of those (and assume if one is missing it's because I didn't read fast enough)</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">clockState</span> <span class="o">==</span> <span class="mb">0b1011</span> <span class="o">||</span> <span class="n">clockState</span> <span class="o">==</span> <span class="mb">0b1101</span> <span class="o">||</span> <span class="n">clockState</span> <span class="o">==</span> <span class="mb">0b1110</span> <span class="o">||</span> <span class="n">clockState</span> <span class="o">==</span> <span class="mb">0b1111</span><span class="p">){</span>
      <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"Result was clockwise"</span><span class="p">);</span>
    <span class="p">}</span>
    <span class="k">if</span> <span class="p">(</span><span class="n">counterClockState</span> <span class="o">==</span> <span class="mb">0b1011</span> <span class="o">||</span> <span class="n">counterClockState</span> <span class="o">==</span> <span class="mb">0b1101</span> <span class="o">||</span> <span class="n">counterClockState</span> <span class="o">==</span> <span class="mb">0b1110</span> <span class="o">||</span> <span class="n">counterClockState</span> <span class="o">==</span> <span class="mb">0b1111</span><span class="p">){</span>
      <span class="n">Serial</span><span class="p">.</span><span class="n">println</span><span class="p">(</span><span class="s">"Result was COUNTER clockwise"</span><span class="p">);</span>
    <span class="p">}</span>
    
    <span class="n">clockState</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    <span class="n">counterClockState</span> <span class="o">=</span> <span class="mi">0</span><span class="p">;</span>
    
  <span class="p">}</span>
  
  <span class="n">prevVal</span> <span class="o">=</span> <span class="n">newVal</span><span class="p">;</span>
<span class="p">}</span></code></pre></figure>

<p>This seems to work! I get a single “result was blah” for every turn, and it seems to be the correct rotation, too! The remaining issue depends on the other code I use this with. This code assumes I’ll be able to read at least 3 out of the 4 states the rotary encoder goes through every time it turns left or right. If I’m checking the encoder in a main loop, and my other code goes really slow, I may only get one or two states. An alternative to this would be setting up an interrupt, so it jumps to reading the encoder every time it changes state. However, interrupts are outside the scope of this blog post, so I’ll leave that as an exercise for the reader.</p>

<h2 id="conclusion">Conclusion</h2>

<p>Hopefully this gives more info on what to expect when using a rotary encoder in your future projects. It’s a lot more complicated than a potentiometer, but that lovely clicky feeling can be perfect for switching between a bunch of options. And now you have the knowledge to use them. So go forth and get clicky!</p>

<h2 id="resources">Resources</h2>

<ul>
  <li>Datasheet for the rotary encoder <a href="https://cdn-shop.adafruit.com/datasheets/pec11.pdf">https://cdn-shop.adafruit.com/datasheets/pec11.pdf</a></li>
  <li>Sparkfun document talking about reading rotary encoders<a href="https://cdn.sparkfun.com/datasheets/Robotics/How%20to%20use%20a%20quadrature%20encoder.pdf">https://cdn.sparkfun.com/datasheets/Robotics/How%20to%20use%20a%20quadrature%20encoder.pdf</a></li>
  <li>Learning about the Arduino IDE serial plotter <a href="https://diyrobocars.com/2020/05/04/arduino-serial-plotter-the-missing-manual/">https://diyrobocars.com/2020/05/04/arduino-serial-plotter-the-missing-manual/</a></li>
  <li>Adafruit’s example using a rotary encoder<a href="https://learn.adafruit.com/pro-trinket-rotary-encoder/example-rotary-encoder-volume-control">https://learn.adafruit.com/pro-trinket-rotary-encoder/example-rotary-encoder-volume-control</a></li>
  <li>Document about rotary encoders <a href="https://web.archive.org/web/20120208215116/http://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino">https://web.archive.org/web/20120208215116/http://www.circuitsathome.com/mcu/reading-rotary-encoder-on-arduino</a></li>
  <li>Pinout for the Adafruit Huzzah with ESP8266 <a href="https://cdn-learn.adafruit.com/assets/assets/000/046/211/original/Huzzah_ESP8266_Pinout_v1.2.pdf?1504807178">https://cdn-learn.adafruit.com/assets/assets/000/046/211/original/Huzzah_ESP8266_Pinout_v1.2.pdf?1504807178</a></li>
</ul>]]></content><author><name></name></author><category term="technical write up" /><category term="side project" /><category term="How-to" /><category term="side project" /><category term="microcontrollers" /><category term="embedded" /><summary type="html"><![CDATA[As part of a project I’m working on, I wanted to have a nice rotating switch that let me flip between different options. Googling around, I discovered that apparently meant I needed a rotary encoder. I bought one from adafruit, only to realize I had no idea how to use it. And oddly enough, Adafruit didn’t have a pre-existing codebase I could pull from. It was time to read some datasheets, do some experiments, and figure out how to use it.]]></summary></entry><entry><title type="html">Setting up ESP8266 with VSCode, Arduino, and Make</title><link href="/blog/2021/8/26/esp8266-with-VSCode-and-arduino" rel="alternate" type="text/html" title="Setting up ESP8266 with VSCode, Arduino, and Make" /><published>2021-08-26T00:00:00+00:00</published><updated>2021-08-26T00:00:00+00:00</updated><id>/blog/2021/8/26/esp8266-with-VSCode-and-arduino</id><content type="html" xml:base="/blog/2021/8/26/esp8266-with-VSCode-and-arduino"><![CDATA[<p>Continuing my exploration of embedded systems, I decided to try my hand at the ESP8266, a popular microcontroller. It’s apparently being phased out and replaced with the ESP32, but it still has a lot of fuctionality, and perhaps more importantly, I already had one on a dev board laying around.</p>

<p>Because I’m all about learning, and also about doing things the hard way, I decided to use a different toolchain setup than what Adafruit suggests (which is just using the arduino ecosystem). Instead, I’ll go for something a little more complicated…</p>

<!--more-->

<h2 id="plan">Plan</h2>
<p>The plan is to use the ESP8266, in this case on the <a href="https://www.adafruit.com/product/2821">adafruit feather Huzzah with ESP8266</a>, with the Arduino libraries, but using VSCode instead of the Arduino IDE. In the spirit of KISS (Keep It Simple Stupid), I decided to use Make to glue everything together. Happily I didn’t have to do this all from scratch, and mainly had to tweak things instead. Let’s get into it.</p>

<h2 id="benefits">Benefits</h2>
<p>Why am I even doing this in the first place? Well:</p>

<ul>
  <li><strong>Arduino Libraries are easy to use</strong> - I <em>could</em> work on getting espressif’s SDK for the ESP8266 going, and figure out how to flash the code onto the huzzah myself. But frankly, that’s a bit too much reinventing the wheel right now. I want to get something functional, and the code I have already uses Arduino libraries and calls, so why re-write it all? Plus, getting more libraries is really easy with the Arduino IDE.</li>
  <li><strong>More Useful IDE</strong> - this is very subjective, but frankly, I like VSCode a lot better than the Arduino IDE. I got a file explorer, a terminal, and a text editor with syntax highlighting, what more could a gal need? Its level of flexibility means I can do everything I want in it (including writing this blog post). I don’t have to relearn a new IDE for embedded stuff, I can stick with what I know. Given all there is to know about embedded systems, I’m willing to skip the less interesting bits.</li>
  <li><strong>Testing</strong> - I did googling but I couldn’t find anything about doing unit tests in the Arduino IDE. I want to build more complex programs, which means testing. Debugging boneheaded errors because I <em>couldn’t</em> do unit tests seemed very silly. If I’m going to debug boneheaded errors, it’s because I <em>decided</em> not to do unit tests. And again, there’s a lot for me to learn here, doing unit tests as I go will help me on my embedded systems adventure.</li>
  <li><strong>Flexibility</strong> - If I want to have multiple builds, or add extra things to my build process, I can! I have to argue with make about it, but it’s at least possible, and kind of already a given with the tools I’m using.</li>
  <li><strong>Local</strong> - I know there’s a couple online embedded IDE’s out there (Platformio comes to mind), but call me old-fashioned: I like to have code on my computer. Sure, I’ll back it up (normally to github), but having a local copy, and not relying on an online cloud service, feels like one less thing to worry about. If my code breaks or disappears, it’s because of me, not because a cloud service decided they wanted more money and paywalled me out of my code.</li>
</ul>

<h2 id="the-software">The Software</h2>

<h3 id="arduino-ide">Arduino IDE</h3>
<p>The arduino IDE is actually still needed for this setup. I know, I know, it seems silly to download something you don’t plan on using. But it does provide easy access to arduino libraries, plus a nice serial monitor and plotter. I don’t like coding in it, but those are nice features to have, so it’s simpler to just download it and use the bits you care about. Download the software directly from Arduino on their <a href="https://www.arduino.cc/en/software">software download page</a>. Make sure to select the correct version, and don’t get distracted by the web editor they’re advertising. You want the downloadable version.</p>

<h3 id="vscode">VSCode</h3>
<p>VSCode, aka Visual Studio Code, is made by Microsoft, and is completely different than their large and somewhat unwieldy Visual Studio IDE. Yes, Microsoft is still terrible at naming things. VSCode is actually open source, and free for private and commercial use. It also has a lot of plugins, built-in terminal, syntax highlighting, and is pretty lightweight for how useful it is. Think of it as somewhere between Sublime and Eclipse/Visual Studio IDE. It also runs on Mac, Linux, and Windows. You can download it from <a href="https://code.visualstudio.com/">Microsoft’s  visual studio code website</a></p>

<h3 id="makefile">Makefile</h3>
<p>Make is old-school (sorry if you’re reading this and remember when it was new. But like, it’s true. You’re old-school now. It came out in ‘76!) It’s a build automation tool that runs on the command line. You can make custom production, dev, test, whatever-else build processes. You can probably get it to clean your kitchen. The commands can be arcane and confusing, but it can do a little bit of everything if you learn the right incantation.
If you have linux, you probably already have it.</p>

<h3 id="the-others">The others</h3>
<p>I’m also using git, python, and g++. The pre-made makefile also calls some perl scripts. Just to give you a full understanding of what I’m using.</p>

<h2 id="the-set-up">The Set-up</h2>

<p>First, make sure you have all of the above listed software. Make sure they all seem to work. Now lets start tweaking things.</p>

<h3 id="arduino-libraries">Arduino Libraries</h3>
<p>Since you want to work on an ESP8266, first you’ll have to download some libraries. The bare-minimum is the ESP8266 board library. This is from <a href="https://github.com/esp8266/Arduino">https://github.com/esp8266/Arduino</a>, and the install instructions are pretty straightforward. Add a new board manager URL, then download the ESP8266 board.</p>

<p>To make sure it works, I’d suggest trying to flash one of the example sketches to the esp8266, make it blink or run a wifi scan.</p>

<h3 id="vscode-1">VSCode</h3>
<p>I Installed a Microsoft-written plugin that helps with syntax: <a href="https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools">https://marketplace.visualstudio.com/items?itemName=ms-vscode.cpptools</a>. The webpage shows the install instructions. Later there’s some VSCode config files you can add to your project to make its error alerts more useful.</p>

<h3 id="makefile-1">Makefile</h3>
<p>There’s actually two makefiles for this. The first is a pre-built one that came from <a href="https://github.com/plerup/makeEspArduino">https://github.com/plerup/makeEspArduino</a>. This has install instructions, and works! But it didn’t support how I setup my project (see below), so I forked it and modified it. It’s now available on my <a href="https://github.com/dthurow/makeEspArduino">github repo</a> with the change I had to make.</p>

<p>That’s the first makefile, the second is a custom one that will live in your project. I’ll explain more below.</p>

<h2 id="project-setup">Project setup</h2>

<p>Folder setup described below. -&gt; means it’s a directory, tabs indicate which files are in what folder (e.g. “unity” folder is inside the “test” folder).</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>-&gt;Project name
    -&gt;.vscode
    -&gt;build
    -&gt;doc
    -&gt;test
        -&gt;unity
            unity_internals.h
            unity.c
            unity.h
    -&gt;src
    -&gt;inc
    -&gt;makeEspArduino
    makefile
    readme.md
    .gitignore
    .gitmodules
</code></pre></div></div>

<p>I’ll go through each of these folder and files to explain what’s in them.</p>

<h3 id="makeesparduino">makeEspArduino</h3>
<p>This is actually a git submodule. This folder contains the <a href="https://github.com/dthurow/makeEspArduino">https://github.com/dthurow/makeEspArduino</a> repo. It lets you keep all your code in one place. The <code class="language-plaintext highlighter-rouge">.gitmodules</code> file (explained below) automatically pulls that in when you do <code class="language-plaintext highlighter-rouge">git clone --recurse-submodules</code> on any project you make following this setup.</p>

<h3 id="gitmodules">.gitmodules</h3>
<p>This lets you have git repos in folders inside other git repos. In this case, we’re adding the <a href="https://github.com/dthurow/makeEspArduino">makeEspArduino</a> into the project. The contents of this file are:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>[submodule "makeEspArduino"]
	path = makeEspArduino
	url = git@github.com:Dthurow/makeEspArduino.git
</code></pre></div></div>

<h3 id="makefile-2">makefile</h3>
<p>This file does two things:</p>
<ol>
  <li>Sets the configuration variables that the <code class="language-plaintext highlighter-rouge">makeEspArduino</code> uses</li>
  <li>Sets up the ability to run tests (I use the <a href="http://www.throwtheswitch.org/unity">unity framework</a>)</li>
</ol>

<p>To make this work for you, you’ll have to make sure that:</p>
<ul>
  <li>the <code class="language-plaintext highlighter-rouge">LIBS</code> directories are where your arduino libraries are.</li>
  <li>The <code class="language-plaintext highlighter-rouge">SKETCH</code> variable is set to your main file (that contains your <code class="language-plaintext highlighter-rouge">setup()</code> and <code class="language-plaintext highlighter-rouge">loop()</code> functions)</li>
  <li>Have g++ downloaded</li>
  <li>Set the <code class="language-plaintext highlighter-rouge">BOARD</code> to the correct value. To figure out what it should be, you can run <code class="language-plaintext highlighter-rouge">make list_boards</code> and the <code class="language-plaintext highlighter-rouge">makeESPArduino</code> file will spit out a list of accepted boards. It also has a <code class="language-plaintext highlighter-rouge">generic</code> option if yours isn’t on the list</li>
</ul>

<figure class="highlight"><pre><code class="language-make" data-lang="make"><span class="nv">DIR</span> <span class="o">:=</span> <span class="nv">${CURDIR}</span>
<span class="nv">SKETCH</span> <span class="o">=</span> <span class="nv">$(DIR)</span>/src/[YOUR MAIN FILE HERE]
<span class="nv">BUILD_DIR</span> <span class="o">=</span> <span class="nv">$(DIR)</span>/build
<span class="nv">BOARD</span> <span class="o">=</span> huzzah
<span class="nv">LIBS</span> <span class="o">=</span> <span class="nv">$(HOME)</span>/.arduino15/packages/esp8266/hardware/esp8266/2.7.4/libraries <span class="nv">$(HOME)</span>/Arduino/libraries 
<span class="nv">EXCLUDE_DIRS</span> <span class="o">=</span> <span class="nb">test</span>
<span class="k">include</span><span class="sx"> $(DIR)/makeEspArduino/makeEspArduino.mk</span>

<span class="c">#everything below here is for setting up tests
</span><span class="nv">TARGET_EXTENSION</span><span class="o">=</span>.out
<span class="c">#Path Definitions
</span><span class="nv">PATHU</span> <span class="o">=</span> <span class="nb">test</span>/unity/
<span class="nv">PATHS</span> <span class="o">=</span> src/
<span class="nv">PATHT</span> <span class="o">=</span> <span class="nb">test</span>/
<span class="nv">PATHI</span> <span class="o">=</span> inc/
<span class="nv">PATHB</span> <span class="o">=</span> build/

<span class="c">#determine our source files
</span><span class="nv">SRCU</span> <span class="o">=</span> <span class="nv">$(PATHU)</span>unity.c
<span class="nv">SRCS</span> <span class="o">=</span> <span class="nf">$(</span><span class="nb">wildcard</span> <span class="nv">$(PATHS)</span><span class="k">*</span>.cpp<span class="nf">)</span>
<span class="nv">SRCT</span> <span class="o">=</span> <span class="nf">$(</span><span class="nb">wildcard</span> <span class="nv">$(PATHT)</span><span class="k">*</span>.cpp<span class="nf">)</span>
<span class="nv">SRC</span> <span class="o">=</span> <span class="nv">$(SRCU)</span> <span class="nv">$(SRCS)</span> <span class="nv">$(SRCT)</span>

<span class="c">#Files We Are To Work With
</span><span class="nv">OBJU</span> <span class="o">=</span> <span class="nf">$(</span><span class="nb">patsubst</span> <span class="nv">$(PATHU)</span>%.c,<span class="nv">$(PATHB)</span>%.o,<span class="nv">$(SRCU)</span><span class="nf">)</span>
<span class="nv">OBJS</span> <span class="o">=</span> <span class="nf">$(</span><span class="nb">patsubst</span> <span class="nv">$(PATHS)</span>%.cpp,<span class="nv">$(PATHB)</span>%.o,<span class="nv">$(SRCS)</span><span class="nf">)</span>
<span class="nv">OBJT</span> <span class="o">=</span> <span class="nf">$(</span><span class="nb">patsubst</span> <span class="nv">$(PATHT)</span>%.cpp,<span class="nv">$(PATHB)</span>%.o,<span class="nv">$(SRCT)</span><span class="nf">)</span>
<span class="nv">OBJ</span> <span class="o">=</span> <span class="nv">$(OBJU)</span> <span class="nv">$(OBJS)</span> <span class="nv">$(OBJT)</span>

<span class="c">#Other files we care about
</span><span class="nv">DEP</span> <span class="o">=</span> <span class="nv">$(PATHU)</span>unity.h <span class="nv">$(PATHU)</span>unity_internals.h
<span class="nv">TGT</span> <span class="o">=</span> <span class="nv">$(PATHB)</span><span class="nb">test</span><span class="nv">$(TARGET_EXTENSION)</span>

<span class="c">#Tool Definitions
</span><span class="nv">CC</span><span class="o">=</span>g++
<span class="nv">CFLAGS</span><span class="o">=</span><span class="nt">-I</span><span class="nb">.</span> <span class="nt">-I</span><span class="nv">$(PATHU)</span> <span class="nt">-I</span><span class="nv">$(PATHI)</span> <span class="nt">-I</span><span class="nv">$(PATHS)</span> <span class="nt">-DTEST</span>

<span class="nl">test</span><span class="o">:</span> <span class="nf">$(PATHB) $(TGT)</span>
	<span class="nb">echo</span> <span class="s2">"running tests"</span>
	./<span class="nv">$(TGT)</span>

<span class="nl">$(PATHB)%.o</span><span class="o">::</span> <span class="nf">$(PATHS)%.cpp $(DEP)</span>
	<span class="nb">echo</span> <span class="s2">"source compiling"</span>
	<span class="nv">$(CC)</span> <span class="nt">-c</span> <span class="nv">$(CFLAGS)</span> <span class="nv">$&lt;</span> <span class="nt">-o</span> <span class="nv">$@</span>

<span class="nl">$(PATHB)%.o</span><span class="o">::</span> <span class="nf">$(PATHT)%.cpp $(DEP)</span>
	<span class="nb">echo</span> <span class="s2">"tests compiling"</span>
	<span class="nv">$(CC)</span> <span class="nt">-c</span> <span class="nv">$(CFLAGS)</span> <span class="nv">$&lt;</span> <span class="nt">-o</span> <span class="nv">$@</span>

<span class="nl">$(PATHB)%.o</span><span class="o">::</span> <span class="nf">$(PATHU)%.c $(DEP)</span>
	<span class="nb">echo</span> <span class="s2">"unity compiling"</span>
	<span class="nv">$(CC)</span> <span class="nt">-c</span> <span class="nv">$(CFLAGS)</span> <span class="nv">$&lt;</span> <span class="nt">-o</span> <span class="nv">$@</span>

<span class="nl">$(TGT)</span><span class="o">:</span> <span class="nf">$(OBJ)</span>
	<span class="nb">echo</span> <span class="s2">"linking"</span>
	<span class="nv">$(CC)</span> <span class="nt">-o</span> <span class="nv">$@</span> <span class="nv">$^</span>

<span class="nl">.PHONY</span><span class="o">:</span> <span class="nf">test</span></code></pre></figure>

<h3 id="test">test</h3>
<p>This folder is where you’ll put your test files (e.g. <code class="language-plaintext highlighter-rouge">testMyCoolCode.cpp</code>). Your tests can use the unity framework, which you can find out more about here: <a href="http://www.throwtheswitch.org/unity">http://www.throwtheswitch.org/unity</a>. You only need the three files I list in the folder setup, and you can get the latest from github <a href="https://github.com/ThrowTheSwitch/Unity/tree/master/src">https://github.com/ThrowTheSwitch/Unity/tree/master/src</a>.</p>

<h3 id="vscode-2">.vscode</h3>
<p>This folder stores VSCode-specific config files that override your default configs on a project-specific basis. The <code class="language-plaintext highlighter-rouge">makeEspArduino</code> makefile actually has a command that auto-creates this info for you. Once the makefile and your main source file are created, you can run <code class="language-plaintext highlighter-rouge">make vscode</code> and it will autogenerate your <code class="language-plaintext highlighter-rouge">.vscode</code> folder. In my case, though, to get VSCode’s syntax checker to fully work, I had to update the <code class="language-plaintext highlighter-rouge">includePath</code> in the <code class="language-plaintext highlighter-rouge">c_cpp_properties.json</code> file. I updated mine to:</p>
<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>            "includePath": [
                "${env:HOME}/.arduino15/packages/esp8266/hardware/esp8266/2.7.4/tools/sdk/include",
                "${env:HOME}/.arduino15/packages/esp8266/hardware/esp8266/2.7.4/tools/sdk/lwip2/include",
                "${env:HOME}/.arduino15/packages/esp8266/hardware/esp8266/2.7.4/tools/sdk/libc/xtensa-lx106-elf/include",
                "${env:HOME}/.arduino15/packages/esp8266/hardware/esp8266/2.7.4/cores/esp8266",
                "${env:HOME}/.arduino15/packages/esp8266/hardware/esp8266/2.7.4/variants/generic",
                "${env:HOME}/.arduino15/packages/esp8266/hardware/esp8266/2.7.4/libraries/**",
                "${env:HOME}/Arduino/libraries/**",
                "${workspaceFolder}/build",
                "${workspaceFolder}/src"
                
            ],
</code></pre></div></div>
<p>And then VSCode no longer errored out on not seeing the Arduino Library folders.</p>

<h3 id="gitignore">.gitignore</h3>
<p>This one’s simple, just ignore the build folder and the special vscode folder:</p>

<div class="language-plaintext highlighter-rouge"><div class="highlight"><pre class="highlight"><code>build
.vscode
</code></pre></div></div>

<h3 id="src">src</h3>
<p>Where your actual production code lives! I know, there’s a lot of other <em>stuff</em> this project has, but honest, it’s all there for a good reason.</p>

<h3 id="inc">inc</h3>
<p>Honestly I’m on the fence if you really need this folder. This can be used to include needed header files. You can also just keep them in <code class="language-plaintext highlighter-rouge">src</code>, I won’t tell.</p>

<h3 id="readmemd">readme.md</h3>
<p>A good file for any project, so you can write up setup or contribution info.</p>

<h3 id="build">build</h3>
<p>This is where the makefiles will build out any intermediary files and where it’ll put the file compiled version of the code. You can clear this folder out by typing <code class="language-plaintext highlighter-rouge">make clean</code>. Sometimes the default <code class="language-plaintext highlighter-rouge">make all</code> errors because of linker issues. In that case, run a <code class="language-plaintext highlighter-rouge">make clean</code> then <code class="language-plaintext highlighter-rouge">make all</code>.</p>

<h3 id="doc">doc</h3>
<p>A place to put any documentation you may have! You do keep written documentation, right? Because “self-documenting code” isn’t actually a thing? Yeah of course you do, good job you.</p>

<h1 id="the-final-result">The Final Result</h1>

<p>Hopefully, a working project! I’ve put together the template on github <a href="https://github.com/Dthurow/generic-esp8266-project-with-vscode-and-arduino">https://github.com/Dthurow/generic-esp8266-project-with-vscode-and-arduino</a> that you can pull down if you want to do that instead of building it out manually. Please do pull requests or open issues with any updates you may have, I’m curious if this project will be useful for others, or if there’s other ways of doing things I just don’t know about.</p>

<p>Good luck and happy programming!</p>]]></content><author><name></name></author><category term="technical write up" /><category term="side project" /><category term="How-to" /><category term="side project" /><category term="microcontrollers" /><category term="embedded" /><summary type="html"><![CDATA[Continuing my exploration of embedded systems, I decided to try my hand at the ESP8266, a popular microcontroller. It’s apparently being phased out and replaced with the ESP32, but it still has a lot of fuctionality, and perhaps more importantly, I already had one on a dev board laying around. Because I’m all about learning, and also about doing things the hard way, I decided to use a different toolchain setup than what Adafruit suggests (which is just using the arduino ecosystem). Instead, I’ll go for something a little more complicated…]]></summary></entry></feed>