Build a Rotary Dial USB Accessory

Have you ever wanted a reprogrammable dial that you can use to speed through tasks on your computer?

We saw Adafruit’s CircuitPython Media Dial and were inspired to riff on their design a bit. While this post won’t be nearly as nice a tutorial as they prepared, you should be able to hack our design files to build one yourself.

Design Strategy

We loved Adafruit’s idea of a light-up rotary interface for your computer, but while we were adapting it to Treehopper, there were a few things we wanted to change:

  • Shorter and wider. Lower the visual (and physical) center of mass of the dial to make it more ergonomic and less cluttered.
  • More substantial. With a diameter of more than 80mm (3.25″), our design carries a visual weight the lures you into it. We glued pieces of metal inside to add weight, and used an oversized R12 2RS 3/4″-bore bearing.
  • Distraction-free LEDs. The LEDs are installed in a ring around the base — shining outward — instead of upward into the user’s face.
  • Simplified form. Three solid pieces that can be printed on a standard, single-extruder 3D printer with no support material.

Mechanical Design

Treehopper was designed for easy, clean integration into enclosures for camera-ready prototyping. When designing enclosures with Treehopper, be sure to take advantage of some useful features:

  • Geometrically-simple USB C connector with large overhang. Treehopper’s USB connector is a great mechanical feature to reference off of; it protrudes enough from the PCB to flush-mount it to the outside of metal or plastic enclosures, and the rounded-rectangle shape is easy to machine or 3D print — and also forgiving to assemble.
  • Castellated pads provide catches for screws. To keep the board small, Treehopper doesn’t have large, dedicated mounting holes. However, we use an oversized castellated radius that mates nicely with #1, M1.8, or M2-sized screws. Typically tying down PWM3 and Pin 10 is sufficient.

We designed three separate pieces: the main plate that everything is anchored to, the top knob, and a transparent cover on the bottom.

You can grab the Autodesk Fusion 360 files here.

We printed the parts on a cheap Creality CR-10 3D printer. The main plate and top knob were printed simultaneously in black filament; we then swapped in some T-Glase translucent filament to print the bottom cover.


We used a couple of #1-72 screws to secure the board to the main plate. The standard EC-12-style rotary encoder we used is panel-mount with a 1/4″ knob, so we simply pushed it through the main plate and mounted it from the other side.

We bought a high-density meter of APA-102 (Adafruit sells them as DotStar) addressable LEDs, snipped off a strip of 32 of them, and put them around the base. They have a self-adhesive back that was good-but-not-great, so a bit of hot glue was used to keep things under control.

Electrically, it doesn’t get simpler than this project. We ran a ground wire from Treehopper to the COM (middle) post of the encoder side of the EC-12, and onto one of the switch terminals. We attached the two encoder signals to pins 12 and 13. The switch was attached to Pin 7 (PWM1). Because Treehopper has always-on, built-in weak pull-ups on all digital inputs, we didn’t need additional circuitry for the rotary encoder.

Because Treehopper has power and ground terminals on both sides, we simply soldered the APA-102C through to the SPI port on the board, as well as the ground and 3.3V. The APA-102 isn’t specified for 3.3V operation, but we’ve never seen any issues, and the constant-current drivers tend to run a bit cooler as they don’t have to drop as much voltage across them.

Technically, the R12-2RS bearing is optional, but it dramatically improves the user experience with the product. Without the bearing, touching the knob anywhere except the center will put an eccentric force on the rotary encoder, which simply isn’t designed to handle that. This will cause the knob to wibble-wobble around, making the product feel cheap and hard to use.

The only R12-2RS bearings we had laying around were sealed, full of grease. We popped the seals off and cleaned out the grease to reduce drag.

We don’t have good tolerances dialed in with our cheapo 3D printer, so instead of trying to print a fixture to hold the bearing, we installed the bearing into the knob first, dabbed some hot glue onto the bottom of the bearing, and then quickly installed the knob onto the rotary encoder before the glue set.

Finally, we installed the translucent base over the LED assembly. We used standard #4 Plastite screws.

Plastite screws are thread-forming screws built specifically to go into plastic — they’re not widely available in hardware stores, but they’re available online. These screws seem to grab into plastic much better than the commonly-available sheet metal screws that people use, so we try to use them whenever possible.

We put a round stick-on foam pad on the bottom to prevent it from scuffing desks.


Since we’ll want to be able to do low-level OS calls to fire off key-presses or mouse events, we’ll write this in C#, targetting .NET 4.6.1 or later. Why? .NET has built-in support for a lot of low-level Windows routines, and even if it doesn’t, you can call into native code with a simple [DllImport] statement. Python and (especially) Java are much more painful in this regard.

While a production version of this product would have a nice GUI used to configure the device, for now, let’s just build a console app and hard-code everything — that way, we can get going quickly. It’ll be easy enough to refactor later.

Our screen shots will be from Visual Studio 2017 in Windows, but note that you can accomplish the same tasks (and use exactly the same code — up to a point) in Visual Studio for Mac, MonoDevelop for Linux, or Rider.

Fire up Visual Studio and create a new C# Console app.

Right-click on the project and choose Manage NuGet Packages…

Search for Treehopper, and install the Treehopper, Treehopper.Desktop, and Treehopper.Libraries packages (for what it’s worth, both Treehopper.Desktop and Treehopper.Libraries will pull in Treehopper automatically, so there’s no need to explicitly install it). Recall that Treehopper.Desktop provides connectivity for class apps on macOS, Linux, and Windows, and Treehopper.Libraries contains drivers for the APA102 and RotaryEncoder (among others).

For starters, let’s make a class called RotaryDial that will contain the code we need. Right-click on the project, and choose Add > New Item… and choose Class. Visual Studio will generate a skeleton class with some using statements at the top.

Let’s get the rotary encoder to drive the LEDs. We’ll make an async Run() method:

public async Task Run()
  var board = await ConnectionService.Instance.GetFirstDeviceAsync();
  await board.ConnectAsync();
  var driver = new Apa102(board.Spi, 32);
  driver.AutoFlush = false;
  var encoder = new RotaryEncoder(board.Pins[13], board.Pins[12], 4);
  encoder.PositionChanged += async(s, e) =>
      var encoderAngle = 360 * e.NewPosition / 20f; // 20 clicks per rotation
      for (int i = 0; i < driver.Leds.Count; i++)
        driver.Leds[i].SetHsl((360f / driver.Leds.Count) * i + encoderAngle, 100, 50);

      await driver.FlushAsync();

With built-in library support for rotary encoders and DotStar-compatible LED light strips, the basic interfacing is a breeze. Here, we instantiate APA102 and RotaryEncoder objects, and then add an event handler for the encoder’s PositionChanged event. We used a lambda, but Visual Studio will auto-generate a separate method for you if you allow it.

Inside the event handler, we convert encoder clicks to degrees (there are 20 clicks per revolution for our particular encoder).

All RGB LEDs in Treehopperland have a SetRgb() method, but we’ll use the SetHsl() method instead. The main property we’ll set is the hue (we’ll leave saturation all the way up for colors that pop, and we can set luminance to a value that won’t blind us).

By binning 360 degrees into the number of LEDs we have (32 in our case), we can assign a hue value to each LED that will draw a continuous rainbow around the entire strip.

Then, if we want to rotate that by a certain angle, we can simply add in an offset. The hue is moduloed by the driver automatically, so you can pass it arbitrary positive or negative numbers and things will wrap around properly. Consequently, we’ll simply take the encoder degrees and add that as a DC offset hue.

Visual Studio will remind you to add some using statements at the top:

using Treehopper;
using Treehopper.Libraries.Displays;
using Treehopper.Libraries.Input;

But that’s about it.

Let’s switch over to the Program.cs file and run our method:

new RotaryDial().Run();

By separating our code into its own RotaryDial class, we can move it over to a different project type in the future.

At this point, here’s what the code is doing:

Making it do something

Now, getting the dial to actually do something takes a bit more effort. There’s a zillion ways to command 3rd-party applications from your own app, and it really comes down to what you want to do.

Here are some ideas:

  • Write an Adobe Lightroom plugin that uses the rotary dial to control slider settings. Page forward and backward through different sliders with click and double-click events.
  • Send targetted WM_VSCROLL messages to your PDF reader’s window so your rotary knob can be used to scroll through datasheets — regardless of what your keyboard/mouse are actually focused on.
  • Create a pop-up hotkey app that appears whenever you press and hold the button — allowing you to scroll through different hotkeys and release the button to activate.

While these are cool project ideas, they’re outside the scope of this tutorial. Instead, we’ll simply program the dial to send keyboard or mouse messages to scroll the page.

We can easily send key presses to other applications using SendKeys.Send() (or, for our not-running-in-a-form console application, SendKeys.SendWait().)

As a trivial example, we can make the rotary dial send UP and DOWN key presses:

if (e.NewPosition > oldPosition)
if (e.NewPosition < oldPosition)
oldPosition = e.NewPosition;

This works in some applications, but if we want to send actual scroll messages, we need to be able to send mouse events.

While there’s no .NET-specific way of accomplishing this, there’s an ancient User32 function that’s just the ticket: mouse_event(). We can easily import this probably-deprecated DLL function with a single statement:

public static extern void mouse_event(uint flags, int x, int y, int data, int extraInfo);

Consulting the documentation, we should send 0x0800 events with either +120 or -120 data values:

if (e.NewPosition > oldPosition)
  mouse_event(0x0800, 0, 0, 120, 0);
if (e.NewPosition < oldPosition)
  mouse_event(0x0800, 0, 0, -120, 0);
oldPosition = e.NewPosition;

Sure enough, this works a treat. Here’s a video if it scrolling through a web page:

We hope this post gets you thinking about human-computer interaction projects with Treehopper — we’d love to see some more advanced projects built around simple interfaces like this rotary dial, so if you get anything interesting working, ping us with some details!

Leave a comment