USB Rotary Media Dial
2024-07-16 | By Adafruit Industries
License: See Original Project 3D Printing LED Strips RP2040 STEMMA
Courtesy of Adafruit
Guide by Ruiz Brothers
Overview
Build a custom USB media dial using Adafruit’s STEMMA QT Rotary Encoder, QT Py RP2040, and CircuitPython.
Set it up as a media controller to adjust your computer's volume or customize it to be a macropad with keyboard shortcuts.
Embedded underneath the 3D printed knob is a NeoPixel strip that changes color whenever it’s turned up or down.
With a rotary encoder you can make a single-click to play and pause media. Double-click to skip to the next track and long press to mute the volume.
Parts
- Adafruit QT Py RP2040
- Adafruit I2C Stemma QT Rotary Encoder Breakout with Encoder
- Adafruit NeoPixel LED Side Light Strip - Black 120 LED
- STEMMA QT / Qwiic JST SH 4-Pin Cable - 50mm Long
- Pink and Purple Woven USB A to USB C Cable - 1 meter long
- 1 x Silicone Cover Ribbon Cable
- 4 x M2.5 x 6mm Screws
Circuit Diagram
The diagram below provides a general visual reference for wiring of the components once you get to the Assembly page. This diagram was created using the software package Fritzing.
Adafruit Library for Fritzing
Adafruit uses the Adafruit's Fritzing parts library to create circuit diagrams for projects. You can download the library or just grab individual parts. Get the library and parts from GitHub - Adafruit Fritzing Parts.
Wired Connections
- Rotary STEMMA QT connects to STEMMA QT port on QT Py RP2040
- GND from NeoPixel strip connects to GND pin on QT Py RP2040
- DIN from NeoPixel strip connects to A1 on QT Py RP2040
- 5V from NeoPixel strip connects to 5V on QT Py RP2040
5V USB Power
The QT Py RP2040 is powered by a computer's 5V USB hub.
CAD Files
3D Printed Parts
STL files for 3D printing are oriented to print "as-is" on FDM style machines. Parts are designed to 3D print without any support material using PLA filament. Original design source may be downloaded using the links below.
Build Volume
The parts require a 3D printer with a minimum build volume.
- 66mm (X) x 66mm (Y) x 35mm (Z)
CAD Assembly
The QT Py RP2040 press fits into the built-in holder on the bottom cover. The case snap fits over the bottom cover. The NeoPixel LED strip fits into the top channel on the case. The STEMMA QT rotary encoder is secured to the built-in standoffs on the bottom cover. The knob press fits onto the shaft of the rotary encoder.
Multi-Color Knob
The knob is printed in multiple colors using the change filament technique. In CURA slicer, open the post processing plugin window by going to the top file menu:
Extensions > Post Processing > Modify GCode
Click, Add a script and choose Filament Change.
Enter 5 in the layer input box for the first change. Then, add another filament change and enter 15 in the layer input box.
Filament Swap
Use a dark colored filament for the first six layers. When layer 6 finishes, the printer will pause to allow the filament to be swapped.
Use a white or clear colored filament for layers 6 through 15. When the printer pauses the second time, change the filament back to the dark colored filament.
Design Source Files
The project assembly was designed in Fusion 360. This can be downloaded in different formats like STEP, STL and more.
Electronic components like Adafruit's boards, displays, connectors and more can be downloaded from the Adafruit CAD parts GitHub Repo.
CircuitPython
CircuitPython is a derivative of MicroPython designed to simplify experimentation and education on low-cost microcontrollers. It makes it easier than ever to get prototyping by requiring no upfront desktop software downloads. Simply copy and edit files on the CIRCUITPY drive to iterate.
CircuitPython Quickstart
Follow this step-by-step to quickly get CircuitPython running on your board.
Download the latest version of CircuitPython for this board via circuitpython.org
Click the link above to download the latest CircuitPython UF2 file.
Save it wherever is convenient for you.
To enter the bootloader, hold down the BOOT/BOOTSEL button (highlighted in red above), and while continuing to hold it (don't let go!), press and release the reset button (highlighted in blue above). Continue to hold the BOOT/BOOTSEL button until the RPI-RP2 drive appears!
If the drive does not appear, release all the buttons, and then repeat the process above.
You can also start with your board unplugged from USB, press and hold the BOOTSEL button (highlighted in red above), continue to hold it while plugging it into USB, and wait for the drive to appear before releasing the button.
A lot of people end up using charge-only USB cables and it is very frustrating! Make sure you have a USB cable you know is good for data sync.
You will see a new disk drive appear called RPI-RP2.
Drag the adafruit_circuitpython_etc.uf2 file to RPI-RP2.
The RPI-RP2 drive will disappear, and a new disk drive called CIRCUITPY will appear.
That's it, you're done! :)
Safe Mode
You want to edit your code.py or modify the files on your CIRCUITPY drive but find that you can't. Perhaps your board has gotten into a state where CIRCUITPY is read-only. You may have turned off the CIRCUITPY drive altogether. Whatever the reason, safe mode can help.
Safe mode in CircuitPython does not run any user code on startup and disables auto-reload. This means a few things. First, safe mode bypasses any code in boot.py (where you can set CIRCUITPY read-only or turn it off completely). Second, it does not run the code in code.py. And finally, it does not automatically soft-reload when data is written to the CIRCUITPY drive.
Therefore, whatever you may have done to put your board in a non-interactive state, safe mode gives you the opportunity to correct it without losing all of the data on the CIRCUITPY drive.
Entering Safe Mode
To enter safe mode when using CircuitPython, plug in your board or hit reset (highlighted in red above). Immediately after the board starts up or resets, it waits 1000ms. On some boards, the onboard status LED (highlighted in green above) will blink yellow during that time. If you press reset during that 1000ms, the board will start up in safe mode. It can be difficult to react to the yellow LED, so you may want to think of it simply as a slow double click of the reset button. (Remember, a fast double click of reset enters the bootloader.)
In Safe Mode
If you successfully enter safe mode on CircuitPython, the LED will intermittently blink yellow three times.
If you connect to the serial console, you'll find the following message.
Auto-reload is off.
Running in safe mode! Not running saved code.
CircuitPython is in safe mode because you pressed the reset button during boot. Press again to exit safe mode.
Press any key to enter the REPL. Use CTRL-D to reload.
You can now edit the contents of the CIRCUITPY drive. Remember, your code will not run until you press the reset button, or unplug and plug in your board, to get out of safe mode.
Flash Resetting UF2
If your board ever gets into a really weird state and CIRCUITPY doesn't show up as a disk drive after installing CircuitPython, try loading this 'nuke' UF2 to RPI-RP2. which will do a 'deep clean' on your Flash Memory. You will lose all the files on the board, but at least you'll be able to revive it! After loading this UF2, follow the steps above to re-install CircuitPython.
Download flash erasing "nuke" UF2
Code the Media Dial
Once you've finished setting up your RP2040 QT Py with CircuitPython, you can access the code and necessary libraries by downloading the Project Bundle.
To do this, click on the Download Project Bundle button in the window below. It will download to your computer as a zipped folder.
# SPDX-FileCopyrightText: 2024 Liz Clark for Adafruit Industries
#
# SPDX-License-Identifier: MIT
import board
import usb_hid
import neopixel
from rainbowio import colorwheel
from adafruit_debouncer import Button
from adafruit_seesaw import seesaw, rotaryio, digitalio
from adafruit_hid.consumer_control import ConsumerControl
from adafruit_hid.consumer_control_code import ConsumerControlCode
from adafruit_hid.keyboard import Keyboard
from adafruit_hid.keycode import Keycode
enc_inc = ConsumerControlCode.VOLUME_INCREMENT
enc_dec = ConsumerControlCode.VOLUME_DECREMENT
one_press = ConsumerControlCode.PLAY_PAUSE
two_press = ConsumerControlCode.SCAN_NEXT_TRACK
three_press = [Keycode.LEFT_CONTROL, Keycode.UP_ARROW]
long_press = ConsumerControlCode.MUTE
cc = ConsumerControl(usb_hid.devices)
kbd = Keyboard(usb_hid.devices)
pixel_pin = board.A1
num_pixels = 18
pixels = neopixel.NeoPixel(pixel_pin, num_pixels,
brightness=.5, auto_write=True)
hue = 0
pixels.fill(colorwheel(hue))
i2c = board.STEMMA_I2C()
seesaw = seesaw.Seesaw(i2c, 0x36)
seesaw.pin_mode(24, seesaw.INPUT_PULLUP)
ss_pin = digitalio.DigitalIO(seesaw, 24)
button = Button(ss_pin, long_duration_ms=600)
encoder = rotaryio.IncrementalEncoder(seesaw)
last_position = 0
while True:
position = -encoder.position
button.update()
if position != last_position:
if position > last_position:
cc.send(enc_dec)
hue = hue - 7
if hue <= 0:
hue = hue + 256
else:
cc.send(enc_inc)
hue = hue + 7
if hue >= 256:
hue = hue - 256
pixels.fill(colorwheel(hue))
last_position = position
if button.short_count == 1:
cc.send(one_press)
if button.short_count == 2:
cc.send(two_press)
if button.short_count == 3:
kbd.press(*three_press)
kbd.release_all()
if button.long_press:
cc.send(long_press)
Upload the Code and Libraries to the RP2040 QT Py
After downloading the Project Bundle, plug your RP2040 QT Py into the computer's USB port with a known good USB data+power cable. You should see a new flash drive appear in the computer's File Explorer or Finder (depending on your operating system) called CIRCUITPY. Unzip the folder and copy the following items to the Feather's CIRCUITPY drive:
- lib folder
- code.py
Your RP2040 QT Py CIRCUITPY drive should look like this after copying the lib folder and the code.py file.
How the CircuitPython Code Works
At the top of the code are variables for the keycodes that are sent by the media dial. You can update these keycodes if you want the media dial to do different things. By default, turn the encoder increases or decreases volume, one short press sends play/pause, two short presses send next track, three short presses send the mission control keyboard short for MacOS and a long press mutes your speaker volume.
enc_inc = ConsumerControlCode.VOLUME_INCREMENT
enc_dec = ConsumerControlCode.VOLUME_DECREMENT
one_press = ConsumerControlCode.PLAY_PAUSE
two_press = ConsumerControlCode.SCAN_NEXT_TRACK
three_press = [Keycode.LEFT_CONTROL, Keycode.UP_ARROW]
long_press = ConsumerControlCode.MUTE
HID, NeoPixels and seesaw
The Keyboard USB HID device is instantiated, followed by the NeoPixels and seesaw rotary encoder.
cc = ConsumerControl(usb_hid.devices)
kbd = Keyboard(usb_hid.devices)
pixel_pin = board.A1
num_pixels = 18
pixels = neopixel.NeoPixel(pixel_pin, num_pixels,
brightness=.5, auto_write=True)
hue = 0
pixels.fill(colorwheel(hue))
i2c = board.STEMMA_I2C()
seesaw = seesaw.Seesaw(i2c, 0x36)
seesaw.pin_mode(24, seesaw.INPUT_PULLUP)
ss_pin = digitalio.DigitalIO(seesaw, 24)
button = Button(ss_pin, long_duration_ms=600)
encoder = rotaryio.IncrementalEncoder(seesaw)
last_position = 0
The Loop
In the loop, the rotary encoder and its switch are monitored for inputs. If the encoder is turned, then the volume is increased or decreased. The NeoPixels will cycle through the colorwheel colors.
if position != last_position:
if position > last_position:
cc.send(enc_dec)
hue = hue - 7
if hue <= 0:
hue = hue + 256
else:
cc.send(enc_inc)
hue = hue + 7
if hue >= 256:
hue = hue - 256
pixels.fill(colorwheel(hue))
last_position = position
The switch on the encoder is defined as an adafruit_debouncer Button object, allowing you to define a long press duration and count short presses. When the defined presses are detected in the loop, the associated keycodes are sent.
if button.short_count == 1:
cc.send(one_press)
if button.short_count == 2:
cc.send(two_press)
if button.short_count == 3:
kbd.press(*three_press)
kbd.release_all()
if button.long_press:
cc.send(long_press)
List of USB HID Keycodes
A list of available keyboard characters and consumer controls are listed in the documentation linked below. Function keys and modifiers can be used to create custom keyboard shortcuts.
Wiring Assembly
Parts
Get the parts ready to wire and assemble.
NeoPixel Strip
Start by removing the NeoPixel strip from the reel.
Remove the stock cable from the beginning of the strip using wire cutters.
Measure and cut a piece from the strip so it's about 6 inches (152mm) in length. This should have 18 NeoPixel LEDs in the cut piece.
Remove the silicone sheathing from the cut piece.
NeoPixel Wire
Measure and cut a piece from the 4-wire silicone ribbon cable so it's 4 inches (102mm) in length.
Peel off a single wire from the cable so it's only 3 wires.
Using wire strippers, remove a bit of insulation from each wire.
Tin the exposed wire using solder to prevent the strands of wire from fraying.
Solder Wire to NeoPixel
Locate the end of the NeoPixel strip with the DI (Data In) label.
Solder the three wires from the cable to the pads on the back of the NeoPixel strip.
Double check the wires are properly soldered to 5V, DI and GND pads on the NeoPixel LED strip.
Wire QT Py
Make the following connections to the QT Py RP2040.
- GND from NeoPixel to GND on QT Py
- DI from NeoPixel strip to A1 on QT Py
- 5V from NeoPixel strip to 5V on QT Py
QT Py Bottom Cover
Get the QT Py and bottom cover ready to assemble.
Orient the flat edge of the bottom cover with the USB port on the QT Py.
Install QT Py to Bottom
Fit the QT Py PCB in between the four standoffs.
Insert the QT Py at an angle to fit one side of the PCB is fitted underneath the built-in clips.
Carefully flex the bottom cover to allow the other side of the QT Py PCB to fit other the built-in clips.
Connect STEMMA QT
Plug in the short STEMMA QT cable to the STEMMA QT port on the QT Py RP2040.
Install Rotary STEMMA QT
Place the Rotary STEMMA QT breakout over the bottom covers built-in standoffs with the mounting holes lined up.
Orient the breakout so that STEMMA QT ports are is lined up.
Secure Rotary STEMMA QT
Use four M2.5 x 6mm steel machine screws to secure the Rotary STEMMA QT breakout to the bottom cover.
Connect QT Py to Rotary STEMMA QT
Plug in the STEMMA QT cable from the Rotary Breakout to the QT Py RP2040.
Install Case
Pull the NeoPixel strip through the 3D printed case.
Orient the USB cutout with the QT Py's USB-C port.
Firmly press the bottom cover and case to snap fit them closed.
USB Port
The QT Py's USB-C port is accessible on the side of the case.
Install NeoPixel Strip
Orient the NeoPixel strip so the LEDs are facing up.
Carefully fit the NeoPixel strip into the inner channel on the top of the case.
Install Knob
Orient the 3D printed knob so the flat edge is lined up with the shaft of the rotary encoder.
Press the knob into the shaft of the rotary encoder until the rotary encoder clicks.
Final Build
Congratulations on your build! Rotate the knob to check if everything is lined properly.
Usage
USB HID Controls
- Turn the encoder increases or decreases volume.
- Single click to play/pause media.
- Double click to skip to next track.
- Triple click to envoke the mission control keyboard shortcut for MacOS.
- Long press to mute your speaker volume.
Customize USB HID Keycodes
You can update these keycodes if you want the media dial to do different things.
A list of keycodes and consumer controls are listed in the documentation linked below. Function keys and modifiers can be used to create custom keyboard shortcuts. Just change the keycodes in the variables located in the top of the code.
enc_inc = ConsumerControlCode.VOLUME_INCREMENT
enc_dec = ConsumerControlCode.VOLUME_DECREMENT
one_press = ConsumerControlCode.PLAY_PAUSE
two_press = ConsumerControlCode.SCAN_NEXT_TRACK
three_press = [Keycode.LEFT_CONTROL, Keycode.UP_ARROW]
long_press = ConsumerControlCode.MUTE
Have questions or comments? Continue the conversation on TechForum, DigiKey's online community and technical resource.
Visit TechForum